<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="cs">
  <title>🌹 ruza · writing</title>
  <subtitle>Personal hub - links, GPG key, presence across the interwebs and possibly some articles.</subtitle>
  <link href="https://ruza.eu/" rel="alternate" type="text/html"/>
  <link href="https://ruza.eu/feed.xml" rel="self" type="application/atom+xml"/>
  <id>https://ruza.eu/feed.xml</id>
  <updated>2026-06-18T00:00:00Z</updated>
  <author>
    <name>ruza</name>
    <uri>https://ruza.eu/</uri>
  </author>
  <generator uri="https://ruza.eu/">build.py</generator>
  <entry>
    <title>The 2-Second Habit That Cuts Your AI Costs</title>
    <link href="https://ruza.eu/articles/the-2-second-habit-that-cuts-your-ai-costs.html" rel="alternate" type="text/html"/>
    <id>https://ruza.eu/articles/the-2-second-habit-that-cuts-your-ai-costs.html</id>
    <published>2026-06-18T00:00:00Z</published>
    <updated>2026-06-18T00:00:00Z</updated>
    <summary>You&apos;re Wasting AI Tokens Without Knowing It — Here&apos;s the Fix</summary>
    <content type="html"><![CDATA[
<h1 id="building-a-session-motd-for-claude-code">Building a Session MOTD for Claude Code</h1>
<h2 id="why-this-matters-sustainability-and-cost">Why This Matters: Sustainability and Cost</h2>
<p>Every AI request burns tokens — and tokens have both a financial cost and an environmental one. Data centers running large language models consume significant electricity and water for cooling. Wasting tokens isn&rsquo;t just expensive; it&rsquo;s an unnecessary environmental burden.</p>
<p>The most common source of silent waste isn&rsquo;t bad prompts — it&rsquo;s <strong>forgetting what settings you left on</strong>. The biggest quiet token drains are:</p>
<ul>
<li><strong>Forgotten expensive model</strong> — starting a trivial task with Opus or Fable instead of Haiku because you forgot to switch back</li>
<li><strong>High effort left on</strong> — effort level <code>high</code> or <code>xhigh</code> multiplies token usage for every request</li>
<li><strong>Fast mode enabled</strong> — faster output means more parallel compute, higher cost</li>
<li><strong>Thinking always on</strong> — extended reasoning for simple tasks where it adds no value</li>
<li><strong>Unnecessary MCP servers active</strong> — each connected server injects thousands of tokens of tool definitions into every request, before you even type your first message</li>
</ul>
<p>The fix isn&rsquo;t complex tooling. It&rsquo;s a two-second habit: <strong>glance at your settings before starting a new task</strong>.</p>
<p>This MOTD implementation makes that glance effortless. You see the current state automatically at session start, color-coded so expensive settings jump out immediately.</p>
<hr>
<p>A lightweight way to display your current session configuration — model, effort, fast mode, thinking, and active MCP servers — every time a Claude Code session starts, and on demand via <code>/motd</code>.</p>
<h2 id="what-it-does">What It Does</h2>
<p>At session start (and whenever you type <code>/motd</code>), Claude Code shows a colored status block:</p>
<p><img loading="lazy" decoding="async" alt="MOTD preview" src="claude-code-motd-preview.svg"></p>
<p>Color coding highlights expensive settings at a glance:
- <strong>Red</strong> = Opus/Fable model, fast mode on, high effort
- <strong>Yellow</strong> = Sonnet model, medium effort, MCP servers active
- <strong>Green</strong> = Haiku model, low effort, fast off</p>
<hr>
<h2 id="files-to-create">Files to Create</h2>
<table>
<thead>
<tr>
<th>File</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>~/.claude/hooks/session-motd.sh</code></td>
<td>Script that generates the colored MOTD</td>
</tr>
<tr>
<td><code>~/.claude/commands/motd.md</code></td>
<td>Slash command <code>/motd</code></td>
</tr>
</tbody>
</table>
<hr>
<h2 id="1-hook-script">1. Hook Script</h2>
<p><code>~/.claude/hooks/session-motd.sh</code>:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">bash</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="ch">#!/usr/bin/env bash</span>

<span class="nv">SETTINGS</span><span class="o">=</span><span class="s2">&quot;</span><span class="nv">$HOME</span><span class="s2">/.claude/settings.json&quot;</span>
get<span class="o">()</span><span class="w"> </span><span class="o">{</span><span class="w"> </span>jq<span class="w"> </span>-r<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$1</span><span class="s2"> // empty&quot;</span><span class="w"> </span><span class="s2">&quot;</span><span class="nv">$SETTINGS</span><span class="s2">&quot;</span><span class="w"> </span><span class="m">2</span>&gt;/dev/null<span class="p">;</span><span class="w"> </span><span class="o">}</span>

<span class="c1"># Colors — must be actual ESC bytes, not literal \033</span>
<span class="nv">RED</span><span class="o">=</span><span class="s1">$&#39;\033[1;31m&#39;</span>
<span class="nv">YLW</span><span class="o">=</span><span class="s1">$&#39;\033[1;33m&#39;</span>
<span class="nv">GRN</span><span class="o">=</span><span class="s1">$&#39;\033[1;32m&#39;</span>
<span class="nv">GRY</span><span class="o">=</span><span class="s1">$&#39;\033[0;90m&#39;</span>
<span class="nv">BLD</span><span class="o">=</span><span class="s1">$&#39;\033[1m&#39;</span>
<span class="nv">RST</span><span class="o">=</span><span class="s1">$&#39;\033[0m&#39;</span>

<span class="c1"># ── MODEL ──────────────────────────────────────────────────────────────</span>
<span class="nv">MODEL_RAW</span><span class="o">=</span><span class="k">$(</span>get<span class="w"> </span><span class="s1">&#39;.model&#39;</span><span class="k">)</span>
<span class="o">[</span><span class="w"> </span>-z<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$MODEL_RAW</span><span class="s2">&quot;</span><span class="w"> </span><span class="o">]</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="nv">MODEL_RAW</span><span class="o">=</span><span class="s2">&quot;sonnet&quot;</span>

<span class="k">case</span><span class="w"> </span><span class="s2">&quot;</span><span class="nv">$MODEL_RAW</span><span class="s2">&quot;</span><span class="w"> </span><span class="k">in</span>
<span class="w">    </span>*opus*<span class="p">|</span>*fable*<span class="o">)</span><span class="w">   </span><span class="nv">MODEL_COL</span><span class="o">=</span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">RED</span><span class="si">}${</span><span class="nv">MODEL_RAW</span><span class="si">}</span><span class="s2"> ⚠ expensive</span><span class="si">${</span><span class="nv">RST</span><span class="si">}</span><span class="s2">&quot;</span><span class="w"> </span><span class="p">;;</span>
<span class="w">    </span>*sonnet*<span class="o">)</span><span class="w">         </span><span class="nv">MODEL_COL</span><span class="o">=</span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">YLW</span><span class="si">}${</span><span class="nv">MODEL_RAW</span><span class="si">}${</span><span class="nv">RST</span><span class="si">}</span><span class="s2">&quot;</span><span class="w"> </span><span class="p">;;</span>
<span class="w">    </span>*haiku*<span class="o">)</span><span class="w">          </span><span class="nv">MODEL_COL</span><span class="o">=</span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">GRN</span><span class="si">}${</span><span class="nv">MODEL_RAW</span><span class="si">}${</span><span class="nv">RST</span><span class="si">}</span><span class="s2">&quot;</span><span class="w"> </span><span class="p">;;</span>
<span class="w">    </span>*<span class="o">)</span><span class="w">                </span><span class="nv">MODEL_COL</span><span class="o">=</span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">YLW</span><span class="si">}${</span><span class="nv">MODEL_RAW</span><span class="si">}${</span><span class="nv">RST</span><span class="si">}</span><span class="s2">&quot;</span><span class="w"> </span><span class="p">;;</span>
<span class="k">esac</span>

<span class="c1"># ── CONTEXT WINDOW ────────────────────────────────────────────────────</span>
<span class="nv">COMPACT_WINDOW</span><span class="o">=</span><span class="k">$(</span>get<span class="w"> </span><span class="s1">&#39;.autoCompactWindow&#39;</span><span class="k">)</span>
<span class="nv">AUTO_COMPACT</span><span class="o">=</span><span class="k">$(</span>get<span class="w"> </span><span class="s1">&#39;.autoCompactEnabled&#39;</span><span class="k">)</span>
<span class="k">if</span><span class="w"> </span><span class="o">[</span><span class="w"> </span>-n<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$COMPACT_WINDOW</span><span class="s2">&quot;</span><span class="w"> </span><span class="o">]</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
<span class="w">    </span><span class="nv">CTX_INFO</span><span class="o">=</span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">COMPACT_WINDOW</span><span class="si">}</span><span class="s2"> tokens&quot;</span>
<span class="w">    </span><span class="o">[</span><span class="w"> </span><span class="s2">&quot;</span><span class="nv">$AUTO_COMPACT</span><span class="s2">&quot;</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&quot;true&quot;</span><span class="w"> </span><span class="o">]</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="nv">CTX_INFO</span><span class="o">=</span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">CTX_INFO</span><span class="si">}</span><span class="s2"> </span><span class="si">${</span><span class="nv">GRN</span><span class="si">}</span><span class="s2">(auto-compact)</span><span class="si">${</span><span class="nv">RST</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="k">else</span>
<span class="w">    </span><span class="nv">CTX_INFO</span><span class="o">=</span><span class="s2">&quot;unknown&quot;</span>
<span class="k">fi</span>

<span class="c1"># ── EFFORT ────────────────────────────────────────────────────────────</span>
<span class="nv">EFFORT</span><span class="o">=</span><span class="k">$(</span>get<span class="w"> </span><span class="s1">&#39;.effortLevel&#39;</span><span class="k">)</span>
<span class="o">[</span><span class="w"> </span>-z<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$EFFORT</span><span class="s2">&quot;</span><span class="w"> </span><span class="o">]</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="nv">EFFORT</span><span class="o">=</span><span class="s2">&quot;medium&quot;</span>
<span class="k">case</span><span class="w"> </span><span class="s2">&quot;</span><span class="nv">$EFFORT</span><span class="s2">&quot;</span><span class="w"> </span><span class="k">in</span>
<span class="w">    </span>xhigh<span class="p">|</span>high<span class="o">)</span><span class="w"> </span><span class="nv">EFFORT_COL</span><span class="o">=</span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">RED</span><span class="si">}${</span><span class="nv">EFFORT</span><span class="si">}</span><span class="s2"> ⚠</span><span class="si">${</span><span class="nv">RST</span><span class="si">}</span><span class="s2">&quot;</span><span class="w"> </span><span class="p">;;</span>
<span class="w">    </span>medium<span class="o">)</span><span class="w">     </span><span class="nv">EFFORT_COL</span><span class="o">=</span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">YLW</span><span class="si">}${</span><span class="nv">EFFORT</span><span class="si">}${</span><span class="nv">RST</span><span class="si">}</span><span class="s2">&quot;</span><span class="w"> </span><span class="p">;;</span>
<span class="w">    </span>low<span class="o">)</span><span class="w">        </span><span class="nv">EFFORT_COL</span><span class="o">=</span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">GRN</span><span class="si">}${</span><span class="nv">EFFORT</span><span class="si">}${</span><span class="nv">RST</span><span class="si">}</span><span class="s2">&quot;</span><span class="w"> </span><span class="p">;;</span>
<span class="w">    </span>*<span class="o">)</span><span class="w">          </span><span class="nv">EFFORT_COL</span><span class="o">=</span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">YLW</span><span class="si">}${</span><span class="nv">EFFORT</span><span class="si">}${</span><span class="nv">RST</span><span class="si">}</span><span class="s2">&quot;</span><span class="w"> </span><span class="p">;;</span>
<span class="k">esac</span>

<span class="c1"># ── FAST MODE ─────────────────────────────────────────────────────────</span>
<span class="nv">FAST</span><span class="o">=</span><span class="k">$(</span>get<span class="w"> </span><span class="s1">&#39;.fastMode&#39;</span><span class="k">)</span>
<span class="k">if</span><span class="w"> </span><span class="o">[</span><span class="w"> </span><span class="s2">&quot;</span><span class="nv">$FAST</span><span class="s2">&quot;</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&quot;true&quot;</span><span class="w"> </span><span class="o">]</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
<span class="w">    </span><span class="nv">FAST_COL</span><span class="o">=</span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">RED</span><span class="si">}</span><span class="s2">ON ⚠</span><span class="si">${</span><span class="nv">RST</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="k">else</span>
<span class="w">    </span><span class="nv">FAST_COL</span><span class="o">=</span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">GRN</span><span class="si">}</span><span class="s2">off</span><span class="si">${</span><span class="nv">RST</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="k">fi</span>

<span class="c1"># ── THINKING ──────────────────────────────────────────────────────────</span>
<span class="nv">THINKING</span><span class="o">=</span><span class="k">$(</span>get<span class="w"> </span><span class="s1">&#39;.alwaysThinkingEnabled&#39;</span><span class="k">)</span>
<span class="k">if</span><span class="w"> </span><span class="o">[</span><span class="w"> </span><span class="s2">&quot;</span><span class="nv">$THINKING</span><span class="s2">&quot;</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&quot;false&quot;</span><span class="w"> </span><span class="o">]</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
<span class="w">    </span><span class="nv">THINK_COL</span><span class="o">=</span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">GRN</span><span class="si">}</span><span class="s2">OFF</span><span class="si">${</span><span class="nv">RST</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="k">elif</span><span class="w"> </span><span class="o">[</span><span class="w"> </span><span class="s2">&quot;</span><span class="nv">$THINKING</span><span class="s2">&quot;</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&quot;true&quot;</span><span class="w"> </span><span class="o">]</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
<span class="w">    </span><span class="nv">THINK_COL</span><span class="o">=</span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">YLW</span><span class="si">}</span><span class="s2">always ON</span><span class="si">${</span><span class="nv">RST</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="k">else</span>
<span class="w">    </span><span class="nv">THINK_COL</span><span class="o">=</span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">GRY</span><span class="si">}</span><span class="s2">auto</span><span class="si">${</span><span class="nv">RST</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="k">fi</span>

<span class="c1"># ── MCP SERVERS ───────────────────────────────────────────────────────</span>
<span class="nv">MCP_FILE</span><span class="o">=</span><span class="s2">&quot;&quot;</span>
<span class="k">for</span><span class="w"> </span>f<span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="s2">&quot;</span><span class="nv">$HOME</span><span class="s2">/.claude/mcp.json&quot;</span><span class="w"> </span><span class="s2">&quot;</span><span class="nv">$HOME</span><span class="s2">/.config/claude/claude_desktop_config.json&quot;</span><span class="p">;</span><span class="w"> </span><span class="k">do</span>
<span class="w">    </span><span class="o">[</span><span class="w"> </span>-f<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$f</span><span class="s2">&quot;</span><span class="w"> </span><span class="o">]</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="o">{</span><span class="w"> </span><span class="nv">MCP_FILE</span><span class="o">=</span><span class="s2">&quot;</span><span class="nv">$f</span><span class="s2">&quot;</span><span class="p">;</span><span class="w"> </span><span class="k">break</span><span class="p">;</span><span class="w"> </span><span class="o">}</span>
<span class="k">done</span>

<span class="nv">MCP_LINE</span><span class="o">=</span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">GRY</span><span class="si">}</span><span class="s2">none</span><span class="si">${</span><span class="nv">RST</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="k">if</span><span class="w"> </span><span class="o">[</span><span class="w"> </span>-n<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$MCP_FILE</span><span class="s2">&quot;</span><span class="w"> </span><span class="o">]</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
<span class="w">    </span><span class="nv">ALL_SERVERS</span><span class="o">=</span><span class="k">$(</span>jq<span class="w"> </span>-r<span class="w"> </span><span class="s1">&#39;(.mcpServers // .mcp_servers // {}) | keys[]&#39;</span><span class="w"> </span><span class="s2">&quot;</span><span class="nv">$MCP_FILE</span><span class="s2">&quot;</span><span class="w"> </span><span class="m">2</span>&gt;/dev/null<span class="k">)</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="o">[</span><span class="w"> </span>-n<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$ALL_SERVERS</span><span class="s2">&quot;</span><span class="w"> </span><span class="o">]</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
<span class="w">        </span><span class="nv">DISABLED</span><span class="o">=</span><span class="k">$(</span>get<span class="w"> </span><span class="s1">&#39;.disabledMcpjsonServers[]?&#39;</span><span class="w"> </span><span class="m">2</span>&gt;/dev/null<span class="w"> </span><span class="p">|</span><span class="w"> </span>tr<span class="w"> </span><span class="s1">&#39;\n&#39;</span><span class="w"> </span><span class="s1">&#39;|&#39;</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>sed<span class="w"> </span><span class="s1">&#39;s/|$//&#39;</span><span class="k">)</span>
<span class="w">        </span><span class="nv">ENABLE_ALL</span><span class="o">=</span><span class="k">$(</span>get<span class="w"> </span><span class="s1">&#39;.enableAllProjectMcpServers&#39;</span><span class="k">)</span>

<span class="w">        </span><span class="nv">PARTS</span><span class="o">=()</span>
<span class="w">        </span><span class="k">while</span><span class="w"> </span><span class="nv">IFS</span><span class="o">=</span><span class="w"> </span><span class="nb">read</span><span class="w"> </span>-r<span class="w"> </span>srv<span class="p">;</span><span class="w"> </span><span class="k">do</span>
<span class="w">            </span><span class="o">[</span><span class="w"> </span>-z<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$srv</span><span class="s2">&quot;</span><span class="w"> </span><span class="o">]</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="k">continue</span>
<span class="w">            </span><span class="k">if</span><span class="w"> </span><span class="o">[</span><span class="w"> </span><span class="s2">&quot;</span><span class="nv">$ENABLE_ALL</span><span class="s2">&quot;</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&quot;true&quot;</span><span class="w"> </span><span class="o">]</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
<span class="w">                </span><span class="nv">PARTS</span><span class="o">+=(</span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">GRN</span><span class="si">}${</span><span class="nv">srv</span><span class="si">}${</span><span class="nv">RST</span><span class="si">}</span><span class="s2">&quot;</span><span class="o">)</span>
<span class="w">            </span><span class="k">elif</span><span class="w"> </span><span class="o">[</span><span class="w"> </span>-n<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$DISABLED</span><span class="s2">&quot;</span><span class="w"> </span><span class="o">]</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;</span><span class="nv">$srv</span><span class="s2">&quot;</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>grep<span class="w"> </span>-qE<span class="w"> </span><span class="s2">&quot;^(</span><span class="si">${</span><span class="nv">DISABLED</span><span class="si">}</span><span class="s2">)</span>$<span class="s2">&quot;</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
<span class="w">                </span><span class="nv">PARTS</span><span class="o">+=(</span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">GRY</span><span class="si">}${</span><span class="nv">srv</span><span class="si">}</span><span class="s2"> (off)</span><span class="si">${</span><span class="nv">RST</span><span class="si">}</span><span class="s2">&quot;</span><span class="o">)</span>
<span class="w">            </span><span class="k">else</span>
<span class="w">                </span><span class="nv">PARTS</span><span class="o">+=(</span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">YLW</span><span class="si">}${</span><span class="nv">srv</span><span class="si">}${</span><span class="nv">RST</span><span class="si">}</span><span class="s2">&quot;</span><span class="o">)</span>
<span class="w">            </span><span class="k">fi</span>
<span class="w">        </span><span class="k">done</span><span class="w"> </span><span class="o">&lt;&lt;&lt;</span><span class="w"> </span><span class="s2">&quot;</span><span class="nv">$ALL_SERVERS</span><span class="s2">&quot;</span>

<span class="w">        </span><span class="nv">MCP_LINE</span><span class="o">=</span><span class="k">$(</span><span class="nb">printf</span><span class="w"> </span><span class="s1">&#39;%s&#39;</span><span class="w"> </span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">PARTS</span><span class="p">[0]</span><span class="si">}</span><span class="s2">&quot;</span><span class="k">)</span>
<span class="w">        </span><span class="k">for</span><span class="w"> </span><span class="o">((</span><span class="nv">i</span><span class="o">=</span><span class="m">1</span><span class="p">;</span><span class="w"> </span>i&lt;<span class="si">${#</span><span class="nv">PARTS</span><span class="p">[@]</span><span class="si">}</span><span class="p">;</span><span class="w"> </span>i++<span class="o">))</span><span class="p">;</span><span class="w"> </span><span class="k">do</span>
<span class="w">            </span><span class="nv">MCP_LINE</span><span class="o">+=</span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">GRY</span><span class="si">}</span><span class="s2">, </span><span class="si">${</span><span class="nv">RST</span><span class="si">}${</span><span class="nv">PARTS</span><span class="p">[</span><span class="nv">$i</span><span class="p">]</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="w">        </span><span class="k">done</span>
<span class="w">    </span><span class="k">fi</span>
<span class="k">fi</span>

<span class="c1"># ── BUILD OUTPUT ──────────────────────────────────────────────────────</span>
<span class="nv">LINE</span><span class="o">=</span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">GRY</span><span class="si">}</span><span class="s2">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</span><span class="si">${</span><span class="nv">RST</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="nv">MSG</span><span class="o">=</span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">LINE</span><span class="si">}</span>
<span class="si">${</span><span class="nv">BLD</span><span class="si">}</span><span class="s2">Model:</span><span class="si">${</span><span class="nv">RST</span><span class="si">}</span><span class="s2">    </span><span class="si">${</span><span class="nv">MODEL_COL</span><span class="si">}</span>
<span class="si">${</span><span class="nv">BLD</span><span class="si">}</span><span class="s2">Context:</span><span class="si">${</span><span class="nv">RST</span><span class="si">}</span><span class="s2">  </span><span class="si">${</span><span class="nv">CTX_INFO</span><span class="si">}</span>
<span class="si">${</span><span class="nv">BLD</span><span class="si">}</span><span class="s2">Effort:</span><span class="si">${</span><span class="nv">RST</span><span class="si">}</span><span class="s2">   </span><span class="si">${</span><span class="nv">EFFORT_COL</span><span class="si">}</span><span class="s2">   </span><span class="si">${</span><span class="nv">BLD</span><span class="si">}</span><span class="s2">Fast:</span><span class="si">${</span><span class="nv">RST</span><span class="si">}</span><span class="s2"> </span><span class="si">${</span><span class="nv">FAST_COL</span><span class="si">}</span><span class="s2">   </span><span class="si">${</span><span class="nv">BLD</span><span class="si">}</span><span class="s2">Thinking:</span><span class="si">${</span><span class="nv">RST</span><span class="si">}</span><span class="s2"> </span><span class="si">${</span><span class="nv">THINK_COL</span><span class="si">}</span>
<span class="si">${</span><span class="nv">BLD</span><span class="si">}</span><span class="s2">MCP:</span><span class="si">${</span><span class="nv">RST</span><span class="si">}</span><span class="s2">      </span><span class="si">${</span><span class="nv">MCP_LINE</span><span class="si">}</span>
<span class="si">${</span><span class="nv">LINE</span><span class="si">}</span><span class="s2">&quot;</span>

jq<span class="w"> </span>-n<span class="w"> </span>--arg<span class="w"> </span>msg<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$MSG</span><span class="s2">&quot;</span><span class="w"> </span><span class="s1">&#39;{&quot;systemMessage&quot;: $msg}&#39;</span>
</code></pre></div></div>

<p>Make the script executable:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">bash</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code>chmod<span class="w"> </span>+x<span class="w"> </span>~/.claude/hooks/session-motd.sh
</code></pre></div></div>

<p><strong>Key implementation detail:</strong> ANSI color variables must use actual ESC bytes (<code>$'\033[...'</code>), not the literal string <code>\033</code>. Using the literal string produces garbled output in Claude Code&rsquo;s terminal renderer.</p>
<hr>
<h2 id="2-slash-command">2. Slash Command</h2>
<p><code>~/.claude/commands/motd.md</code>:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">json</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code>---
<span class="gu">description: Show current session configuration (model, effort, fast, thinking, MCP)</span>
<span class="gu">---</span>

&lt;bash&gt;bash ~/.claude/hooks/session-motd.sh | jq -r &#39;.systemMessage&#39;&lt;/bash&gt;

Print the output of the command above verbatim, without truncation or modification.
It contains ANSI escape codes — pass them through exactly as-is.
</code></pre></div></div>

<p>The instruction to print verbatim is necessary. Without it Claude Code may summarize or strip the escape codes.</p>
<hr>
<h2 id="3-wire-it-into-settingsjson">3. Wire It Into settings.json</h2>
<p>Add the hook to <code>~/.claude/settings.json</code>:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">bash</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w">  </span><span class="nt">&quot;hooks&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nt">&quot;SessionStart&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
<span class="w">      </span><span class="p">{</span>
<span class="w">        </span><span class="nt">&quot;hooks&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
<span class="w">          </span><span class="p">{</span>
<span class="w">            </span><span class="nt">&quot;type&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;command&quot;</span><span class="p">,</span>
<span class="w">            </span><span class="nt">&quot;command&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;bash ~/.claude/hooks/session-motd.sh&quot;</span>
<span class="w">          </span><span class="p">}</span>
<span class="w">        </span><span class="p">]</span>
<span class="w">      </span><span class="p">}</span>
<span class="w">    </span><span class="p">]</span>
<span class="w">  </span><span class="p">},</span>
<span class="w">  </span><span class="nt">&quot;permissions&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nt">&quot;allow&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
<span class="w">      </span><span class="s2">&quot;Bash(bash ~/.claude/hooks/session-motd.sh *)&quot;</span><span class="p">,</span>
<span class="w">      </span><span class="s2">&quot;Bash(jq -r .systemMessage)&quot;</span>
<span class="w">    </span><span class="p">]</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The <code>permissions.allow</code> entries prevent Claude Code from prompting for approval every time <code>/motd</code> is run manually. Without them, the slash command triggers a permission dialog on each invocation.</p>
<hr>
<h2 id="how-the-sessionstart-hook-works">How the SessionStart Hook Works</h2>
<p>When Claude Code starts a session, it runs <code>SessionStart</code> hooks and expects each hook&rsquo;s stdout to be a JSON object. If the object contains a <code>systemMessage</code> key, Claude Code injects that string as a system-level message shown to the user at the top of the session.</p>
<p>The script therefore ends with:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">code</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code>jq<span class="w"> </span>-n<span class="w"> </span>--arg<span class="w"> </span>msg<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$MSG</span><span class="s2">&quot;</span><span class="w"> </span><span class="s1">&#39;{&quot;systemMessage&quot;: $msg}&#39;</span>
</code></pre></div></div>

<p>This is the only output format Claude Code accepts from a hook — a JSON object on stdout.</p>
<hr>
<h2 id="settings-keys-read-by-the-script">Settings Keys Read by the Script</h2>
<p>All values come from <code>~/.claude/settings.json</code>:</p>
<table>
<thead>
<tr>
<th>Key</th>
<th>What it controls</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>.model</code></td>
<td>Active model name</td>
</tr>
<tr>
<td><code>.autoCompactWindow</code></td>
<td>Context window size in tokens</td>
</tr>
<tr>
<td><code>.autoCompactEnabled</code></td>
<td>Whether auto-compact is on</td>
</tr>
<tr>
<td><code>.effortLevel</code></td>
<td><code>low</code> / <code>medium</code> / <code>high</code> / <code>xhigh</code></td>
</tr>
<tr>
<td><code>.fastMode</code></td>
<td>Boolean, fast mode toggle</td>
</tr>
<tr>
<td><code>.alwaysThinkingEnabled</code></td>
<td><code>true</code> / <code>false</code> / absent (auto)</td>
</tr>
<tr>
<td><code>.disabledMcpjsonServers[]</code></td>
<td>List of disabled MCP server names</td>
</tr>
<tr>
<td><code>.enableAllProjectMcpServers</code></td>
<td>Boolean, force-enable all MCP servers</td>
</tr>
</tbody>
</table>
<p>MCP server names are read from <code>~/.claude/mcp.json</code> (or <code>~/.config/claude/claude_desktop_config.json</code> as fallback), under the <code>mcpServers</code> or <code>mcp_servers</code> key.</p>
<hr>
<h2 id="dependencies">Dependencies</h2>
<ul>
<li><code>jq</code> — for reading <code>settings.json</code> and producing the JSON output</li>
<li><code>bash</code> 4+ — for arrays (<code>PARTS=()</code>) and <code>$'\033[..]'</code> syntax</li>
</ul>
<p>Both are available by default on most Linux systems.</p>
<hr>
<h2 id="gotchas">Gotchas</h2>
<p><strong>Unicode box-drawing characters in code blocks</strong> can break rendering in some environments (e.g. DokuWiki). The <code>━</code> separator line works correctly in Claude Code&rsquo;s terminal output.</p>
<p><strong><code>$'\033[..]'</code> vs <code>'\033[..]'</code></strong> — the dollar-sign prefix is required. Without it, the variable contains the literal text <code>\033[1;31m</code> instead of the ESC byte sequence, and colors don&rsquo;t render.</p>
<p><strong>The slash command needs the verbatim instruction</strong> — Claude Code will otherwise interpret and reformat the ANSI output rather than passing it through.</p>
    ]]></content>
    <author>
      <name>ruza</name>
    </author>
  </entry>
  <entry>
    <title>Jak změna Nextcloud instanceid znepřístupnila všechny soubory</title>
    <link href="https://ruza.eu/articles/nextcloud-sifrovani-souboru.html" rel="alternate" type="text/html"/>
    <id>https://ruza.eu/articles/nextcloud-sifrovani-souboru.html</id>
    <published>2026-06-18T00:00:00Z</published>
    <updated>2026-06-18T00:00:00Z</updated>
    <summary>Když změna web server SW je větší drama než by člověk chtěl</summary>
    <content type="html"><![CDATA[
<p><strong>Verze:</strong> Nextcloud 33.0.5.1, kontejner <code>nextcloud:33-fpm</code><br>
<strong>Dopad:</strong> Všechny soubory nepřístupné přes web, Android i desktop klienty</p>
<hr>
<h2 id="priznaky">Příznaky</h2>
<p>Po migraci na nový docker-compose.yaml přestaly fungovat soubory:</p>
<ul>
<li>Webový klient: prázdný adresář nebo chyba při otevírání</li>
<li>Android klient: soubory se nezobrazují, nelze otevřít</li>
<li>Desktop klient: PROPFIND (adresářový výpis) vrací 207, stažení souborů selhává</li>
<li>NC log: <code>Could not boot encryption: Bad Signature</code> a <code>Could not decrypt the private key from user "master_XXXXXXXX"</code></li>
</ul>
<p>Nextcloud jako celek fungoval (přihlášení, nastavení, výpis adresářů). Nedostupné byly pouze šifrované soubory.</p>
<hr>
<h2 id="kontext-proc-novy-docker-compose">Kontext: proč nový docker-compose</h2>
<p>Z důvodu potřeby provozu na slabším hardware jsme se rozhodli migrovat z <code>nextcloud:33-apache</code> na <code>nextcloud:33-fpm</code> s nginxem jako frontendem. O migraci na efektivnější web server (přepnutí na jiný Apache worker) jsem se už pokoušel, ale skončil v dependency hell a vracel se k dosavadnímu řešení. Rozhodl jsem se tedy zkusit přechod na nginx a nechat to udělat Claude code. A to byla chyba. Architektonicky to sice zvládl, ale zapomněl správně zmigrovat konfiguraci a Nextcloud při startu se zachoval nejlépe jak uměl. Výsledkem byl výrazný pokles spotřeby CPU a RAM. Při té příležitosti ale taky vznikl nový docker-compose.yaml s novou konfigurací a právě jeho nasazení incident způsobilo.</p>
<hr>
<h2 id="pozadi-jak-nc-uklada-sifrovany-master-klic">Pozadí: jak NC ukládá šifrovaný master klíč</h2>
<p>Nextcloud server-side encryption v master key módu:</p>
<ol>
<li>Každý soubor je zašifrován náhodným symetrickým klíčem (AES)</li>
<li>Symetrický klíč je zašifrován RSA veřejným klíčem master páru</li>
<li>Výsledek je uložen jako <code>.shareKey</code> vedle souboru</li>
<li>Master RSA privátní klíč je v <code>data/files_encryption/OC_DEFAULT_MODULE/master_XXXXXXXX.privateKey</code></li>
</ol>
<p>Pokud nejde dešifrovat master privátní klíč → nelze dešifrovat žádný shareKey → žádný soubor.</p>
<h3 id="dvouvrstve-sifrovani-master-privatniho-klice">Dvouvrstvé šifrování master privátního klíče</h3>
<p><strong>Vnější vrstva — <code>OC\Security\Crypto</code> (ICrypto), formát <code>hexEnc|hexIV|hexHMAC|3</code>:</strong></p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">code</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code>klíč  = HKDF(SHA-512, config.secret)[0:32]
šifra = phpseclib AES-CBC  ← POZOR: ne OpenSSL, phpseclib!
</code></pre></div></div>

<p>Po dešifrování: JSON <code>{"key": "base64(vnitřní_data)", "uid": null}</code></p>
<p><strong>Vnitřní vrstva — HBEGIN formát (AES-256-CTR, keyFormat:hash):</strong></p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">code</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code>HBEGIN:cipher:AES-256-CTR:keyFormat:hash:encoding:binary:HEND
[binary ciphertext][00iv00][16B IV][00sig00][64B HMAC hex][xxx]
</code></pre></div></div>

<p>Klíč je odvozen přes PBKDF2 (100 000 iterací):</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">php</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="x">$salt = hash(&#39;sha256&#39;, $uid . $instanceId . $secret, true);</span>
<span class="x">$key  = hash_pbkdf2(&#39;sha256&#39;, $secret, $salt, 100000, 32, true);</span>
<span class="x">// $uid        = &#39;master_XXXXXXXX&#39;   ← ID master klíče z DB</span>
<span class="x">// $instanceId = z config.php        ← zde je problém</span>
<span class="x">// $secret     = z config.php</span>
</code></pre></div></div>

<hr>
<h2 id="pricina">Příčina</h2>
<p>Při nasazení nového docker-compose.yaml 2026-06-15 byl kontejner spuštěn v bootstrapovacím módu — a NC <strong>automaticky vygeneroval nové <code>instanceid</code></strong>:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">code</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code>Původní hodnota:  instanceid = &#39;oc1a2b3c4d5e&#39;   (od dubna 2023, nezměněno 3 roky)
Po migraci:       instanceid = &#39;myservername&#39;
</code></pre></div></div>

<p>Původní hodnota má typický tvar NC-generovaného ID (alfanumerický řetězec s prefixem <code>oc</code>). Nová hodnota je naopak čitelný název — podle všeho ji NC bootstrap dosadil z hostname nebo jiné konfigurace serveru, protože <code>config.php</code> nebyl správně přemontován a NC spustil výchozí inicializaci.</p>
<p><code>instanceid</code> vstupuje do PBKDF2 soli → jiná hodnota = jiný odvozený klíč → dešifrování vnitřní vrstvy selže → &ldquo;Bad Signature&rdquo;.</p>
<p>Vnější ICrypto vrstva <strong>nebyla dotčena</strong> (závisí jen na <code>secret</code>, ne na <code>instanceid</code>). Proto šla vnější vrstva dešifrovat, vnitřní ne.</p>
<hr>
<h2 id="postup-zkoumani">Postup zkoumání</h2>
<h3 id="1-identifikace-formatu-souboru">1. Identifikace formátu souboru</h3>
<p>Soubor <code>master_XXXXXXXX.privateKey</code> má formát <code>hexEnc|hexIV|hexHMAC|3</code>. Zpočátku jsem předpokládal starý NC pipe-delimited formát — ale <code>|3</code> je přípona <strong><code>OC\Security\Crypto</code></strong> (ICrypto), ne legacy encryption.</p>
<p>Potvrzení úspěšným dešifrováním vnější vrstvy:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">php</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="x">$inner = $crypto-&gt;decrypt($data);</span>
<span class="x">// → {&quot;key&quot;:&quot;SEJFRzJOOmNpcGhlcjpBRVMtMjU2LUNUUjprZXlGb3JtYXQ6...&quot;}</span>
<span class="x">//     ^^^^^ base64 začátek HBEGIN dat</span>
<span class="x">// ICrypto HMAC: MATCH ✓</span>
</code></pre></div></div>

<h3 id="2-marne-hledani-spravneho-pbkdf2-klice">2. Marné hledání správného PBKDF2 klíče</h3>
<p>Vnitřní HBEGIN data se podařilo parsovat. Jenže odvozený klíč dával garbage:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">code</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code>derivedKey = PBKDF2(secret, sha256(&#39;&#39; + &#39;oc1a2b3c4d5e&#39; + secret), 100k iterací)
→ dešifrování: 3 272 bytů binárního odpadu
</code></pre></div></div>

<p>Zkoušel jsem systematicky: všechny kombinace instanceId (<code>oc1a2b3c4d5e</code>, <code>myservername</code>), různé iterace (100k, 600k, 65536, 1024), různé soli, raw secret jako klíč — HMAC neseděl <strong>ani jednou</strong>.</p>
<h3 id="3-klicovy-objev-uid-master-klice-neni-prazdny-string">3. Klíčový objev: UID master klíče není prázdný string</h3>
<p>Pohled do <code>apps/encryption/lib/KeyManager.php</code>:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">php</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="x">// řádek 117:</span>
<span class="x">$encryptedKey = $this-&gt;crypt-&gt;encryptPrivateKey(</span>
<span class="x">    $keyPair[&#39;privateKey&#39;],</span>
<span class="x">    $this-&gt;getMasterKeyPassword(),</span>
<span class="x">    $this-&gt;masterKeyId        // ← &#39;master_XXXXXXXX&#39;, NE &#39;&#39;</span>
<span class="x">);</span>
</code></pre></div></div>

<p>Všechny testy předpokládaly <code>$uid = ''</code>. Správná hodnota je <code>'master_XXXXXXXX'</code> (ID uložené v NC databázi).</p>
<p>Se správným UID:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">code</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code>salt = sha256(&#39;master_XXXXXXXX&#39; + &#39;oc1a2b3c4d5e&#39; + secret)
key  = PBKDF2(secret, salt, 100 000 iterací, 32 bytů)
→ VALID RSA KEY! bits=4096 ✓
</code></pre></div></div>

<h3 id="4-overeni-pres-nc-vlastni-kod">4. Ověření přes NC vlastní kód</h3>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">php</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="x">$crypt-&gt;decryptPrivateKey($masterKey, $secret, &#39;master_XXXXXXXX&#39;);</span>
<span class="x">// → RSA 4096 bitů ✓</span>
</code></pre></div></div>

<p>S obnoveným <code>instanceid = 'oc1a2b3c4d5e'</code> NC správně dešifruje master klíč.</p>
<hr>
<h2 id="oprava">Oprava</h2>
<p>Díky tomu, že při každé větší změně vytváříme zálohy konfigurace, měli jsme k dispozici <code>config.php.BAK</code> z předchozí verze (NC30, prosinec 2024). Záloha potvrdila správnou hodnotu <code>instanceid</code>. Oprava byla pak přímočará:</p>
<ol>
<li>Z <code>config.php.BAK</code> odečtena správná hodnota <code>instanceid</code></li>
<li>Hodnota zapsána zpět do <code>config.php</code></li>
<li><code>docker restart nextcloud</code></li>
</ol>
<p><strong>Výsledek:</strong>
- Žádné &ldquo;Bad Signature&rdquo; chyby v logu po restartu
- Desktop klient: synchronizace funguje ✓
- Android klient: soubory přístupné ✓
- Webový klient: soubory přístupné ✓</p>
<hr>
<h2 id="zajimavosti">Zajímavosti</h2>
<h3 id="icrypto-nepouziva-openssl">ICrypto nepoužívá OpenSSL</h3>
<p><code>OC\Security\Crypto</code> interně volá <strong>phpseclib</strong> AES, ne PHP <code>openssl_encrypt/decrypt</code>. Klíč se předává přes <code>$cipher-&gt;setPassword()</code> a je odvozený přes HKDF. Naivní pokus dešifrovat přes <code>openssl_decrypt</code> selže, i když máte správný klíč a správné parametry — voláte totiž jinou knihovnu.</p>
<h3 id="format-3-je-icrypto-ne-legacy-oc-sifrovani">Formát <code>|3</code> je ICrypto, ne legacy OC šifrování</h3>
<p>Starší NC encryption modul (soubory shareKey, fileKey) má odlišný formát. Soubor master privátního klíče je obalený ICryptem a vypadá jako <code>hexEnc|hexIV|hexHMAC|3</code> — stejná přípona jako u starého formátu, ale jde o zcela jinou vrstvu.</p>
<h3 id="nc-uklada-klice-jako-json-base64-icrypto">NC ukládá klíče jako JSON + base64 + ICrypto</h3>
<p><code>lib/private/Encryption/Keys/Storage.php</code>:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">php</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="x">$this-&gt;setKey($path, [</span>
<span class="x">    &#39;key&#39; =&gt; base64_encode($key),</span>
<span class="x">    &#39;uid&#39; =&gt; null,</span>
<span class="x">]);</span>
</code></pre></div></div>

<p>Celý JSON je pak zabalen do ICrypto. Proto <code>getSystemUserKey()</code> vrací <code>base64_decode(json["key"])</code> — raw HBEGIN binární data, ne JSON.</p>
<h3 id="hmac-v-hbegin-formatu-zavisi-na-version-position">HMAC v HBEGIN formátu závisí na version + position</h3>
<p>Podpis v HBEGIN formátu:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">php</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="x">// Crypt.php::createSignature():</span>
<span class="x">$passPhrase = hash(&#39;sha512&#39;, $passPhrase . &#39;a&#39;, true);</span>
<span class="x">return hash_hmac(&#39;sha256&#39;, $data, $passPhrase);</span>

<span class="x">// kde $passPhrase = derivedKey . &#39;_&#39; . $version . &#39;_&#39; . $position</span>
<span class="x">// pro decryptPrivateKey: version=0, position=0 → suffix &#39;_0_0&#39;</span>
</code></pre></div></div>

<p>Bez znalosti suffixu <code>_0_0</code> (a <code>'a'</code> na konci) HMAC nesedí. NC ale nejdřív zkusí nový formát (<code>_0_0</code>), pak starý (<code>00</code>), a pokud selžou oba, při <code>enforceSignature=false</code> pokračuje dál a rovnou zkusí dešifrovat.</p>
<h3 id="zmena-instanceid-muze-byt-u-nextcloud-kriticka">Změna instanceid může být u Nextcloud kritická</h3>
<p>NC nevaruje, že ztráta <code>instanceid</code> okamžitě znepřístupní všechna šifrovaná data. Při startu bez <code>config.php</code> (nebo s neplatným) bootstrap tiše vygeneruje nové <code>instanceid</code> a uloží ho. Záloha <code>config.php</code> — zejména <code>instanceid</code> a <code>secret</code> — je pro provoz šifrovaného NC existenčně důležitá.</p>
<hr>
<h2 id="prevence">Prevence</h2>
<ul>
<li>Před každou migrací zálohovat <code>config.php</code> (obsahuje <code>instanceid</code> + <code>secret</code>)</li>
<li>Po velké migraci ihned funkční test: stáhnout jeden soubor přes WebDAV</li>
<li>V docker-compose.yaml mountovat <code>config/</code> jako <strong>bind mount</strong>, ne Docker volume — bootstrap pak nemůže přepsat existující soubor novým</li>
</ul>
<hr>
<p><em>Napsáno 2026-06-18. NC 33.0.5.1, PHP 8.3, nextcloud:33-fpm.</em></p>
    ]]></content>
    <author>
      <name>ruza</name>
    </author>
  </entry>
  <entry>
    <title>An AI Guide for 1700 Operating Systems</title>
    <link href="https://ruza.eu/articles/an-ai-guide-for-1700-operating-systems.html" rel="alternate" type="text/html"/>
    <id>https://ruza.eu/articles/an-ai-guide-for-1700-operating-systems.html</id>
    <published>2026-05-28T00:00:00Z</published>
    <updated>2026-05-28T00:00:00Z</updated>
    <summary>Removing barriers to gaining experience with legacy operating systems</summary>
    <content type="html"><![CDATA[
<h1 id="an-ai-guide-for-1700-operating-systems">An AI Guide for 1700 Operating Systems</h1>
<p><em>How <a href="https://github.com/ruzaq/ai-watcher" target="_blank" rel="noopener noreferrer">AI Watcher</a> and <a href="https://virtualosmuseum.org" target="_blank" rel="noopener noreferrer">virtualosmuseum.org</a> together remove the two barriers between you and the history of computing.</em></p>
<hr>
<p>There are two reasons people don&rsquo;t get hands-on with historical operating systems.</p>
<p>The first is <strong>access</strong>. Setting up CTSS, or TOPS-20, or BeOS, or any of a hundred other landmark systems means tracking down disk images of uncertain provenance, picking the right emulator, finding the right command-line flags, mounting things, configuring things, hoping nothing has rotted in the years since the last person tried. Most people stop here.</p>
<p>The second is <strong>applying knowledge once you&rsquo;re in</strong>. The system boots. You&rsquo;re staring at a prompt that hasn&rsquo;t changed since 1965. The commands you&rsquo;d need to know are documented — in PDFs scanned from physical manuals, in mailing-list archives, in textbooks long out of print. The information <em>exists</em>, but it lives in places where casual exploration goes to die. Most people who clear the first barrier give up at the second.</p>
<p>These are different problems, and they&rsquo;re solved by different tools.</p>
<h2 id="barrier-one-availability-and-setup">Barrier one: availability and setup</h2>
<p><a href="https://virtualosmuseum.org" target="_blank" rel="noopener noreferrer">Virtual OS Museum</a> is the answer to the first problem. It&rsquo;s a curated collection of 1700+ pre-installed operating systems, packaged so that booting any of them is a single click. The emulator is configured. The disks are mounted. The networking, where applicable, is wired up. You pick an exhibit and it runs.</p>
<p>The point isn&rsquo;t that this is <em>clever</em> — though it is — but that it removes the entire setup phase from the experience. The systems were always there. They were always emulatable. They just weren&rsquo;t <em>available</em>. Now they are.</p>
<h2 id="barrier-two-applying-the-knowledge">Barrier two: applying the knowledge</h2>
<p>That&rsquo;s where <a href="https://github.com/ruzaq/ai-watcher" target="_blank" rel="noopener noreferrer">AI Watcher</a> fits in.</p>
<p>AI Watcher is a small tool that does exactly one thing: when you press a keyboard shortcut, it takes a screenshot of the active window, sends it to Claude with a question, and writes the answer into a log viewer in the corner of your screen. It doesn&rsquo;t replace the manuals. It reads them for you, lazily, when you ask. It&rsquo;s a guide looking over your shoulder — quiet until you call on it.</p>
<p>The interaction model matters. There&rsquo;s no popup that steals focus. No new window opening on top of the system you&rsquo;re trying to use. The shortcut is invisible until the answer arrives. You can be mid-command in a 1970s shell and the only thing that changes is a few new lines in the log window beside you.</p>
<p><img loading="lazy" decoding="async" alt="A typical session — Virtual OS Museum running historical systems, AI Watcher log viewer waiting in the corner" src="https://raw.githubusercontent.com/ruzaq/ai-watcher/main/docs/screenshots/00_Hello_world.png"></p>
<p>What&rsquo;s in that screenshot is the moment where both tools matter. COS (Cray Operating System) is shown here, running multiple terminals. Some show prompts you&rsquo;d recognise, some not. OS museum launcher in left upper corner is the museum offering more exhibits. The xterm in the bottom-right is AI Watcher&rsquo;s log — silent right now, ready to fill with explanation the moment a shortcut is pressed.</p>
<h2 id="how-it-actually-works">How it actually works</h2>
<p>Four shortcuts cover the workflow:</p>
<table>
<thead>
<tr>
<th>Shortcut</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Super+A</code></td>
<td>Window screenshot, default question (<em>&ldquo;what is this, what should I do next?&rdquo;</em>)</td>
</tr>
<tr>
<td><code>Super+Shift+A</code></td>
<td>Whole desktop, default question</td>
</tr>
<tr>
<td><code>Super+Q</code></td>
<td>Window screenshot, then a dialog asking for your own question</td>
</tr>
<tr>
<td><code>Super+Shift+Q</code></td>
<td>Whole desktop, custom question</td>
</tr>
</tbody>
</table>
<p><code>Super+A</code> is the bread and butter. You see something — a cryptic prompt, an error you don&rsquo;t understand, a menu in a language nobody speaks any more. You press the key. Claude looks at the screenshot, identifies the system from visual cues and the window title, and writes three or four sentences: <em>what you&rsquo;re looking at, what the next reasonable action is, what to watch out for</em>. The answer arrives in the log window in two to four seconds.</p>
<p><code>Super+Q</code> is for when you have a specific question. <em>&ldquo;Why does every command come back with <code>?</code>?&rdquo;</em> — Claude answers in context of the specific system on screen, which is usually different from any generic web search you&rsquo;d type the same question into.</p>
<h2 id="where-it-shines-where-it-falls-short">Where it shines, where it falls short</h2>
<p>For systems with surviving documentation in Claude&rsquo;s training data — early UNIX variants, DOS, OS/2, classic Mac OS, BeOS, the well-known mainframe environments — the assistance is comparable to a competent engineer who&rsquo;s never used the system but read the manual. Which is to say: very useful.</p>
<p>For systems with thin documentation — niche Smalltalks, research-lab Lisp machines, Oberon and its descendants — the answers get worse, and sometimes Claude confidently invents details. Asking <em>&ldquo;are you certain?&rdquo;</em> helps, as does keeping verification in mind. The combination of &ldquo;AI says this&rdquo; and &ldquo;the system actually behaves like this&rdquo; is the only thing that survives.</p>
<p>For systems that are essentially folklore — undocumented research one-offs, certain Soviet-era mainframes, BeOS deeper internals — Claude tends to admit it doesn&rsquo;t know, which is the most useful behaviour of all. The honest <em>I don&rsquo;t know</em> response happens more often than people expect from these tools, and that&rsquo;s where trust comes from.</p>
<p>The win isn&rsquo;t omniscience. The win is that the museum becomes <em>navigable</em> — exhibits that would otherwise sit untouched because no one wants to read three textbooks to operate them become approachable, one shortcut at a time.</p>
<h2 id="together">Together</h2>
<ul>
<li>Virtual OS Museum makes historical systems <em>available</em>.</li>
<li>AI Watcher makes them <em>approachable</em>.</li>
</ul>
<p>Neither tool replaces the manuals, the archives, or the original literature — and neither tries to. They just remove the two specific barriers that keep most curious people from ever experiencing what the history of computing actually felt like to use.</p>
<p>The result is a museum you can walk through alone.</p>
<hr>
<h2 id="get-started">Get started</h2>
<ol>
<li><strong>Download Virtual OS Museum</strong> from <a href="https://virtualosmuseum.org" target="_blank" rel="noopener noreferrer">virtualosmuseum.org</a> and run it in a hypervisor — VirtualBox / QEMU.</li>
<li><strong>Install AI Watcher</strong> inside that VM environment:
   <code>git clone https://github.com/ruzaq/ai-watcher &amp;&amp; cd ai-watcher &amp;&amp; ./install.sh</code>
   Then drop your Anthropic API key into <code>~/.config/ai-watcher/env</code> — <a href="https://console.anthropic.com/settings/keys" target="_blank" rel="noopener noreferrer">get one here</a>.</li>
<li><strong>Pick a system and start exploring.</strong> Press <code>Super+A</code> whenever something surprises you.</li>
</ol>
<p>Good first stops — all included in the museum, all well-documented enough for Claude to be a useful guide:</p>
<ul>
<li><strong>CTSS</strong> — the original time-sharing system from MIT, 1961, where a lot of modern computing was first sketched out</li>
<li><strong>TOPS-20</strong> — DEC&rsquo;s PDP-10 operating system, beloved by 1970s hackers, the ancestor of much UNIX culture</li>
<li><strong>OS/2</strong> — IBM&rsquo;s deeply engineered PC OS, full of corners worth knowing</li>
<li><strong>BeOS</strong> — the cult multimedia OS that briefly looked like it might be the next Mac OS</li>
<li><strong>Classic Mac OS</strong> — the GUI that taught the personal computer how to behave</li>
<li><strong>IRIX</strong> — SGI&rsquo;s UNIX, where 1990s 3D graphics happened</li>
</ul>
<p>The history of computing is bigger than most people realise. Most of it is locked behind setup pain and undocumented prompts. Two small tools, working together, unlock more of it than either could alone — and the only thing standing between you and a 1965 mainframe is a <code>git clone</code> and a keyboard shortcut.</p>
    ]]></content>
    <author>
      <name>ruza</name>
    </author>
  </entry>
  <entry>
    <title>LLM Wiki nad Obsidianem v QubesOS</title>
    <link href="https://ruza.eu/articles/llm-wiki-obsidian-setup.html" rel="alternate" type="text/html"/>
    <id>https://ruza.eu/articles/llm-wiki-obsidian-setup.html</id>
    <published>2026-05-28T00:00:00Z</published>
    <updated>2026-05-28T00:00:00Z</updated>
    <summary>Teď už nemusíte do svého sekundárniho mozku jen zapisovat, ale můžete si s ním i povídat.</summary>
    <content type="html"><![CDATA[
<h1 id="llm-wiki-nad-obsidianem-v-qubesos">LLM Wiki nad Obsidianem v QubesOS</h1>
<p>Jak jsem rozšířil svůj osobní knowledge management Obsidian vault na perzistentní znalostní bázi pomáhající LLM AI — s qmd, Claude Code a specifiky QubesOS.</p>
<blockquote>
<p>Upozornění: Následující text není všepopisný návod po jednotlivých krocích. Spíš je záznamem na co můžete narazit při implementaci a jak to lze řešit.</p>
</blockquote>
<hr>
<h2 id="co-je-llm-wiki">💡 Co je LLM Wiki</h2>
<p>Andrej Karpathy <a href="https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f" target="_blank" rel="noopener noreferrer">publikoval v dubnu 2026</a> jednoduchý nápad: místo toho aby pokaždé znovu vysvětloval AI co ví, nechá ji jednou přeložit zdrojové materiály do strukturované wiki a pak AI z těhle dat odpovídá.</p>
<p>Rozdíl oproti běžnému RAG je zásadní:</p>
<table>
<thead>
<tr>
<th>Přístup</th>
<th>Znalost</th>
<th>Stav</th>
</tr>
</thead>
<tbody>
<tr>
<td>RAG</td>
<td>Odvozuje pokaždé znovu</td>
<td>Bezstavový</td>
</tr>
<tr>
<td>LLM Wiki</td>
<td>Jednou zkompiluje, tu část, kterou zrovna potřebuje a tu pak čte</td>
<td>Stavový, kumulativní</td>
</tr>
</tbody>
</table>
<p>RAG je jako vzít knihovnu a pokaždé ji celou přečíst. LLM Wiki je jako mít knihovníka, který si udělal vlastní poznámky a index — a odpovídá z nich.</p>
<p>Základní myšlenka:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">code</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code>zdrojové materiály (raw/) → LLM kompiluje → wiki/ → LLM odpovídá
</code></pre></div></div>

<ul>
<li><code>raw/</code> je source of truth, který píšeš ty.</li>
<li><code>wiki/</code> je synthesis layer, který píše výhradně LLM. Nikdy to nezaměňuješ.</li>
</ul>
<hr>
<h2 id="vicemene-zbytecne-obavy">👻 (Víceméně) zbytečné obavy</h2>
<ul>
<li>&ldquo;AI mi udělá zmatek v léta opečovávaných poznámkách v mém knowledge management řešení Obsidian.&rdquo; → Neudělá. Součástí Karpathyho konceptu LLM Wiki je vstupní adredář <code>raw/</code> jako zdroj, ve kterém nic nemění a výstupní (pro LLM AI pomocný) adresář <code>wiki/</code>.</li>
<li>&ldquo;Jakmile začnu měnit strukturu dat něco se rozbije&rdquo;. → Vyloučit to nelze, ale proto si při úpravách počínáme obezřetně a kontrolujeme si zda zamýšlené změny jsou v souladu s tím jak to funguje. Na co je třeba si dát pozor, např. v nastavení Obsidian, je zmíněné dále v textu.</li>
</ul>
<hr>
<h2 id="vychozi-situace-velky-existujici-obsidian-vault">🤔 Výchozí situace: velký existující Obsidian vault</h2>
<p>Standardní Karpathy pattern předpokládá, že začínáš od nuly — vytvoříš <code>raw/</code> a vedle něj <code>wiki/</code>. Pokud ale máš existující Obsidian vault s léty poznámek, narazíš hned na první otázku: kam co patří?</p>
<p>Můj vault (<code>~/Obsidian.SyncThing/</code>) měl přibližně 500 MB obsahu, 1 078 markdown souborů ve 42 složkách a přílohy (obrázky, PDF) přímo v kořeni. Obsah je primárně zaměřen na bezpečnost a IT (Security, IT, pentest, hacking tvoří ~43 % poznámek), dále finance, soukromí, QubesOS, GrapheneOS a různé další poznámky.</p>
<p>Obsidian Vault je synchronizovaný přes <strong>Syncthing</strong> — stejný obsah je dostupný ve dvou QubesOS VM:</p>
<ul>
<li><code>notes</code> (Ubuntu 22.04 LTS, kde běží Obsidian) a</li>
<li><code>ai-anthropic</code> (Debian 13 Trixie, kde běží Claude Code).</li>
</ul>
<p>Syncthing zajišťuje že obě VM vidí vždy stejný stav vaultu, včetně nově přidaných zdrojů a wiki stránek zapsaných agentem. Z toho vyplývá pojmenování složky vaultu: <code>~/Obsidian.SyncThing/</code> — název reflektuje způsob synchronizace.</p>
<h3 id="proc-nemit-wiki-primo-v-koreni-vaultu">Proč nemít wiki/ přímo v kořeni vaultu</h3>
<p>Technicky by šlo mít <code>wiki/</code> jako podsložku kořene vaultu, kde je i zbytek obsahu. Ale vznikají tím problémy:</p>
<ul>
<li>Při promptu &ldquo;projdi všechny soubory&rdquo; musíš vždy explicitně vyloučit <code>wiki/</code> aby se LLM nemátl vlastním výstupem</li>
<li>Totéž platí pro <code>.obsidian/</code>, <code>.claude/</code>, <code>.stfolder</code> a další skryté složky</li>
<li>Vyjímky je snadné opomenout zmínit → agent čte co sám napsal → chyby se kumulují</li>
</ul>
<p>Čistší řešení: přesunout veškerý obsah do <code>raw/</code> a mít jasnou strukturu:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">code</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code>~/Obsidian.SyncThing/
├── raw/                  ← tvůj obsah (vstupy jako source of truth)
├── wiki/                 ← LLM synthesis výstupy (nikdy needituješ ručně)
├── .obsidian/            ← nastavení Obsidianu
├── .claude/              ← instrukce pro Claude Code
├── .agents/              ← instrukce pro Codex a jiné agenty
├── .stfolder             ← Syncthing marker
└── CLAUDE.md             ← instrukce pro agenta platné pro celý vault
</code></pre></div></div>

<hr>
<h2 id="presun-obsahu-do-raw">Přesun obsahu do raw/</h2>
<p>Obsidian má ve výchozím nastavení &ldquo;New link format: Shortest path possible&rdquo; — wikilinky jako <code>[název souboru](nazev-souboru.html)</code> se resolvují vyhledáním jména po celém vaultu bez ohledu na cestu. Přesun souborů do podsložky tedy wikilinky nerozbije.</p>
<p>Před přesunem si ověř nastavení: <strong>Settings → Files &amp; Links → New link format</strong>.</p>
<p>Přesun z příkazové řádky:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">bash</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="nb">cd</span><span class="w"> </span>~/Obsidian.SyncThing

<span class="c1"># Vytvoř cílovou složku</span>
mkdir<span class="w"> </span>raw

<span class="c1"># Přesuň vše kromě skrytých složek a raw samotného</span>
find<span class="w"> </span>.<span class="w"> </span>-maxdepth<span class="w"> </span><span class="m">1</span><span class="w"> </span><span class="se">\</span>
<span class="w">  </span>!<span class="w"> </span>-name<span class="w"> </span><span class="s1">&#39;.&#39;</span><span class="w"> </span><span class="se">\</span>
<span class="w">  </span>!<span class="w"> </span>-name<span class="w"> </span><span class="s1">&#39;raw&#39;</span><span class="w"> </span><span class="se">\</span>
<span class="w">  </span>!<span class="w"> </span>-path<span class="w"> </span><span class="s1">&#39;./.obsidian&#39;</span><span class="w"> </span><span class="se">\</span>
<span class="w">  </span>!<span class="w"> </span>-path<span class="w"> </span><span class="s1">&#39;./.claude&#39;</span><span class="w"> </span><span class="se">\</span>
<span class="w">  </span>!<span class="w"> </span>-path<span class="w"> </span><span class="s1">&#39;./.agents&#39;</span><span class="w"> </span><span class="se">\</span>
<span class="w">  </span>!<span class="w"> </span>-path<span class="w"> </span><span class="s1">&#39;./.stfolder&#39;</span><span class="w"> </span><span class="se">\</span>
<span class="w">  </span>!<span class="w"> </span>-path<span class="w"> </span><span class="s1">&#39;./.trash&#39;</span><span class="w"> </span><span class="se">\</span>
<span class="w">  </span>-exec<span class="w"> </span>mv<span class="w"> </span><span class="o">{}</span><span class="w"> </span>raw/<span class="w"> </span><span class="se">\;</span>
</code></pre></div></div>

<blockquote>
<p><strong>Pozor:</strong> <code>find</code> bez správného vylučování skrytých složek je přesune taky. Vzor výše vylučuje explicitně každou skrytou složku která patří na vault root. Pokud máš jiné skryté složky v kořeni, které nechceš přesouvat, zmiň je ve vyjímkách stejným způsobem. Děláš to jen jednou. Pak už to bude správně.</p>
</blockquote>
<p>Po přesunu zkontroluj v Obsidianu: <strong>Settings → Files &amp; Links → Default location for new attachments</strong> — pokud ukazoval na konkrétní složku, aktualizuj na <code>raw/attachments</code>.</p>
<h3 id="sjednoceni-priloh">Sjednocení příloh</h3>
<p>Přílohy (obrázky, PDF) ze starších importů mohou být roztroušené přímo v <code>raw/</code> místo v <code>raw/attachments/</code>. Přesun je bezpečný díky Shortest path:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">bash</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code>mkdir<span class="w"> </span>-p<span class="w"> </span>~/Obsidian.SyncThing/raw/attachments

find<span class="w"> </span>~/Obsidian.SyncThing/raw/<span class="w"> </span>-maxdepth<span class="w"> </span><span class="m">1</span><span class="w"> </span>-type<span class="w"> </span>f<span class="w"> </span>!<span class="w"> </span>-name<span class="w"> </span><span class="s2">&quot;*.md&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w">  </span>-exec<span class="w"> </span>mv<span class="w"> </span><span class="o">{}</span><span class="w"> </span>~/Obsidian.SyncThing/raw/attachments/<span class="w"> </span><span class="se">\;</span>
</code></pre></div></div>

<p>Pak nastav výchozí umístění příloh: <strong>Settings → Files &amp; Links → Default location for new attachments → &ldquo;In the folder specified below&rdquo;</strong> → <code>raw/attachments</code>.</p>
<h3 id="kontrola-broken-linku">Kontrola broken linků</h3>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">bash</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="nb">cd</span><span class="w"> </span>~/Obsidian.SyncThing

<span class="c1"># Reference v .md souborech</span>
grep<span class="w"> </span>-roh<span class="w"> </span><span class="s1">&#39;!\[\[^\](.html)*\]\]&#39;</span><span class="w"> </span>raw/<span class="w"> </span><span class="se">\</span>
<span class="w">  </span><span class="p">|</span><span class="w"> </span>sed<span class="w"> </span><span class="s1">&#39;s/.*!\[\[//;s/\]\].*//&#39;</span><span class="w"> </span><span class="se">\</span>
<span class="w">  </span><span class="p">|</span><span class="w"> </span>sort<span class="w"> </span>-u<span class="w"> </span>&gt;<span class="w"> </span>/tmp/referenced.txt

<span class="c1"># Skutečné soubory příloh</span>
find<span class="w"> </span>raw/<span class="w"> </span>-type<span class="w"> </span>f<span class="w"> </span>!<span class="w"> </span>-name<span class="w"> </span><span class="s2">&quot;*.md&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w">  </span><span class="p">|</span><span class="w"> </span>xargs<span class="w"> </span>-I<span class="o">{}</span><span class="w"> </span>basename<span class="w"> </span><span class="o">{}</span><span class="w"> </span><span class="se">\</span>
<span class="w">  </span><span class="p">|</span><span class="w"> </span>sort<span class="w"> </span>-u<span class="w"> </span>&gt;<span class="w"> </span>/tmp/existing.txt

<span class="c1"># Rozdíl = rozbité linky</span>
comm<span class="w"> </span>-23<span class="w"> </span>/tmp/referenced.txt<span class="w"> </span>/tmp/existing.txt
</code></pre></div></div>

<p>Prázdný výstup = vše v pořádku.</p>
<hr>
<h2 id="obsidian-skills">💪 Obsidian Skills</h2>
<p>Ve vaultu jsou nainstalované <a href="https://github.com/kepano/obsidian-skills" target="_blank" rel="noopener noreferrer">obsidian-skills</a> od Steph Anga. Každý skill je markdown soubor v </p>
<ul>
<li><code>.claude/skills/&lt;skill-name&gt;/SKILL.md</code> (pro Claude Code) a</li>
<li><code>.agents/skills/&lt;skill-name&gt;/SKILL.md</code> (pro Codex CLI).</li>
</ul>
<p>Obsah je identický, liší se jen discovery path.</p>
<p>Instalace:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">bash</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code>git<span class="w"> </span>clone<span class="w"> </span>https://github.com/kepano/obsidian-skills.git<span class="w"> </span>/tmp/obsidian-skills

mkdir<span class="w"> </span>-p<span class="w"> </span>~/Obsidian.SyncThing/.claude/skills
cp<span class="w"> </span>-r<span class="w"> </span>/tmp/obsidian-skills/skills/*<span class="w"> </span>~/Obsidian.SyncThing/.claude/skills/

mkdir<span class="w"> </span>-p<span class="w"> </span>~/Obsidian.SyncThing/.agents/skills
cp<span class="w"> </span>-r<span class="w"> </span>/tmp/obsidian-skills/skills/*<span class="w"> </span>~/Obsidian.SyncThing/.agents/skills/

rm<span class="w"> </span>-rf<span class="w"> </span>/tmp/obsidian-skills
</code></pre></div></div>

<p>Nainstalované skills:</p>
<table>
<thead>
<tr>
<th>Skill</th>
<th>Účel</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>obsidian-markdown</code></td>
<td>Správný Obsidian-flavored markdown: wikilinky, frontmatter, callouts</td>
</tr>
<tr>
<td><code>obsidian-cli</code></td>
<td>Otevírání souborů v Obsidianu z agenta</td>
</tr>
<tr>
<td><code>obsidian-bases</code></td>
<td>Databázové pohledy (Obsidian Bases)</td>
</tr>
<tr>
<td><code>json-canvas</code></td>
<td>Vizuální mapy znalostí v Canvas formátu</td>
</tr>
<tr>
<td><code>defuddle</code></td>
<td>Čištění web obsahu při ingestu (odstraní navigaci, boilerplate)</td>
</tr>
</tbody>
</table>
<p>Skills cestují s vaultem — při kopírování do jiného VM přes <code>qvm-copy</code> jsou automaticky dostupné bez jakékoliv globální instalace.</p>
<hr>
<h2 id="claudemd-instrukce-pro-agenta">CLAUDE.md: instrukce pro agenta</h2>
<p><code>CLAUDE.md</code> v kořeni vaultu čte Claude Code automaticky při každém spuštění session. Definuje strukturu, pravidla a workflow.</p>
<p>Klíčové části:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">markdown</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="gu">## Struktura vaultu</span>

~/Obsidian.SyncThing/          ← vault root
├── raw/                       ← zdrojový obsah, POUZE čteš
├── wiki/                      ← synthesis layer, POUZE ty píšeš
│   ├── synthesis/             ← shrnutí témat
│   ├── query/                 ← odpovědi na konkrétní dotazy
│   └── index.md               ← přehled co je ve wiki
├── .claude/skills/            ← skills pro Claude Code
└── .obsidian/                 ← NIKDY nesahej

<span class="gu">## Pravidla</span>

<span class="k">-</span><span class="w"> </span>raw/ nikdy neupravuj, nepřejmenovávej, nesmazávej
<span class="k">-</span><span class="w"> </span>wiki/ píšeš výhradně ty, uživatel ji ručně neupravuje
<span class="k">-</span><span class="w"> </span>Při procházení raw/ vždy vyluč wiki/ aby ses nespletl s vlastním výstupem
</code></pre></div></div>

<p>Důležitá je sekce kontextu domény, aby agent nepotřeboval vault pokaždé znovu analyzovat:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">markdown</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="gu">## Kontext vaultu</span>

Primární témata (pro prioritizaci synthesis):
<span class="k">-</span><span class="w"> </span>Bezpečnost, IT, pentest, hacking (~43 % obsahu)
<span class="k">-</span><span class="w"> </span>Finance, soukromí
<span class="k">-</span><span class="w"> </span>QubesOS, GrapheneOS
<span class="k">-</span><span class="w"> </span>Osobní poznámky, recepty a ostatní

Při technických tématech preferuj přesnost před zjednodušením.
</code></pre></div></div>

<h3 id="struktura-wiki">Struktura wiki/</h3>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">code</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code>wiki/
├── synthesis/     ← zkompilovaná témata z raw/
├── query/         ← odpovědi na konkrétní dotazy (archiv)
├── index.md       ← přehled obsahu wiki
└── log.md         ← audit trail co agent dělal a kdy
</code></pre></div></div>

<p><code>wiki/synthesis/</code> obsahuje trvalé stránky kompilované z raw/ obsahu. <code>wiki/query/</code> je archiv odpovědí na jednorázové dotazy — agent ho přidává ale nemaže.</p>
<hr>
<h2 id="qmd-lokalni-search-engine">qmd: lokální search engine</h2>
<p>Při 1 078 souborech nestačí procházet soubory jeden po druhém. <a href="https://github.com/tobi/qmd" target="_blank" rel="noopener noreferrer">qmd</a> je lokální search engine pro markdown soubory s hybridním BM25/vektorovým vyhledáváním, které běží plně on-device. Má MCP server, takže ho Claude Code může volat jako nativní nástroj.</p>
<h3 id="instalace-na-fedora-debian-v-qubesos">Instalace na Fedora / Debian v QubesOS</h3>
<p>Ve výchozí instalaci nemusí být dostupný ani <code>bun</code> ani <code>go</code>. qmd je npm balíček, ale jeho wrapper script vyžaduje <code>bun</code> jako runtime:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">bash</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="c1"># 1. Instalace qmd přes npm</span>
npm<span class="w"> </span>install<span class="w"> </span>-g<span class="w"> </span>@tobilu/qmd

<span class="c1"># 2. Instalace bun (potřebný runtime)</span>
curl<span class="w"> </span>-fsSL<span class="w"> </span>https://bun.sh/install<span class="w"> </span><span class="p">|</span><span class="w"> </span>bash
</code></pre></div></div>

<p>Instalátor bunu automaticky přidá do <code>~/.bashrc</code>:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">bash</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="nb">export</span><span class="w"> </span><span class="nv">BUN_INSTALL</span><span class="o">=</span><span class="s2">&quot;</span><span class="nv">$HOME</span><span class="s2">/.bun&quot;</span>
<span class="nb">export</span><span class="w"> </span><span class="nv">PATH</span><span class="o">=</span><span class="s2">&quot;</span><span class="nv">$BUN_INSTALL</span><span class="s2">/bin:</span><span class="nv">$PATH</span><span class="s2">&quot;</span>
</code></pre></div></div>

<p>Pro fish shell je potřeba přidat ručně do <code>~/.config/fish/config.fish</code>:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">fish</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code>fish_add_path <span class="s2">&quot;</span><span class="nv">$HOME</span><span class="s2">/.bun/bin&quot;</span>
</code></pre></div></div>

<p>Ověření:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">bash</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="nb">export</span><span class="w"> </span><span class="nv">PATH</span><span class="o">=</span><span class="s2">&quot;</span><span class="nv">$HOME</span><span class="s2">/.bun/bin:</span><span class="nv">$PATH</span><span class="s2">&quot;</span>
qmd<span class="w"> </span>--version
<span class="c1"># qmd 0.9.0</span>
</code></pre></div></div>

<h3 id="vytvoreni-kolekci">Vytvoření kolekcí</h3>
<blockquote>
<p><strong>Gotcha:</strong> Syntaxe <code>qmd collection add</code> je odlišná od dokumentace. Správně: první argument je cesta, název je <code>--name</code>.</p>
</blockquote>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">bash</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code>qmd<span class="w"> </span>collection<span class="w"> </span>add<span class="w"> </span>/home/user/Obsidian.SyncThing/raw<span class="w"> </span><span class="se">\</span>
<span class="w">  </span>--name<span class="w"> </span>my-vault<span class="w"> </span>--mask<span class="w"> </span><span class="s2">&quot;**/*.md&quot;</span>

qmd<span class="w"> </span>collection<span class="w"> </span>add<span class="w"> </span>/home/user/Obsidian.SyncThing/wiki<span class="w"> </span><span class="se">\</span>
<span class="w">  </span>--name<span class="w"> </span>wiki<span class="w"> </span>--mask<span class="w"> </span><span class="s2">&quot;**/*.md&quot;</span>
</code></pre></div></div>

<blockquote>
<p><strong>Gotcha:</strong> Nepřidávej kolekce spojené přes <code>&amp;&amp;</code> pokud první selže — zůstane prázdný záznam v databázi. Přidávej každou zvlášť.</p>
</blockquote>
<p>Přidání kontextu (pomáhá při query s re-rankingem):</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">bash</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code>qmd<span class="w"> </span>context<span class="w"> </span>add<span class="w"> </span>qmd://my-vault/<span class="w"> </span><span class="s2">&quot;Obsidian vault raw obsah - bezpecnost/IT/pentest&quot;</span>
qmd<span class="w"> </span>context<span class="w"> </span>add<span class="w"> </span>qmd://wiki/<span class="w"> </span><span class="s2">&quot;LLM wiki synthesis stranky&quot;</span>
</code></pre></div></div>

<h3 id="indexace">Indexace</h3>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">bash</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="c1"># BM25 full-text index (~30 sekund pro 1000 souborů)</span>
qmd<span class="w"> </span>update

<span class="c1"># Vektorové embeddingy — stáhne ~300 MB modely z HuggingFace</span>
<span class="c1"># Spusť na pozadí, zabere 10-20 minut</span>
nohup<span class="w"> </span>qmd<span class="w"> </span>embed<span class="w"> </span>&gt;<span class="w"> </span>/tmp/qmd-embed.log<span class="w"> </span><span class="m">2</span>&gt;<span class="p">&amp;</span><span class="m">1</span><span class="w"> </span><span class="p">&amp;</span>
tail<span class="w"> </span>-f<span class="w"> </span>/tmp/qmd-embed.log
</code></pre></div></div>

<p>Po <code>qmd update</code> funguje <code>qmd search</code> (BM25 full-text). Po <code>qmd embed</code> fungují i <code>qmd vsearch</code> (sémantické vyhledávání) a <code>qmd query</code> (BM25 + vektory + LLM re-ranking).</p>
<h3 id="pouziti">Použití</h3>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">bash</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code>qmd<span class="w"> </span>search<span class="w"> </span><span class="s2">&quot;pentest&quot;</span><span class="w">                          </span><span class="c1"># BM25 keywords</span>
qmd<span class="w"> </span>vsearch<span class="w"> </span><span class="s2">&quot;privilege escalation techniky&quot;</span><span class="w">   </span><span class="c1"># sémantické vyhledávání</span>
qmd<span class="w"> </span>query<span class="w"> </span><span class="s2">&quot;jak detekovat lateral movement&quot;</span><span class="w">    </span><span class="c1"># hybrid + reranking</span>
</code></pre></div></div>

<h3 id="mcp-server-pro-claude-code">MCP server pro Claude Code</h3>
<blockquote>
<p><strong>Gotcha:</strong> <code>mcpServers</code> nepatří do <code>~/.claude/settings.json</code> — validace schématu to odmítne. Správné místo je <code>~/.claude/mcp.json</code>.</p>
</blockquote>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">json</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w">  </span><span class="nt">&quot;mcpServers&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nt">&quot;qmd&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="nt">&quot;command&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;/home/user/.local/bin/qmd&quot;</span><span class="p">,</span>
<span class="w">      </span><span class="nt">&quot;args&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&quot;mcp&quot;</span><span class="p">],</span>
<span class="w">      </span><span class="nt">&quot;env&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="nt">&quot;PATH&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;/home/user/.bun/bin:/usr/local/bin:/usr/bin:/bin&quot;</span>
<span class="w">      </span><span class="p">}</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<blockquote>
<p><strong>Gotcha:</strong> PATH v <code>env</code> musí explicitně obsahovat <code>~/.bun/bin</code> — MCP server se spouští s čistým prostředím a bun jinak nenajde.</p>
</blockquote>
<p>Ověření bez restartu Claude Code (přímý JSON-RPC):</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">bash</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="nb">echo</span><span class="w"> </span><span class="s1">&#39;{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:1,&quot;method&quot;:&quot;tools/call&quot;,&quot;params&quot;:{&quot;name&quot;:&quot;qmd_search&quot;,&quot;arguments&quot;:{&quot;query&quot;:&quot;pentest&quot;}}}&#39;</span><span class="w"> </span><span class="se">\</span>
<span class="w">  </span><span class="p">|</span><span class="w"> </span><span class="nv">PATH</span><span class="o">=</span><span class="s2">&quot;/home/user/.bun/bin:</span><span class="nv">$PATH</span><span class="s2">&quot;</span><span class="w"> </span>/home/user/.local/bin/qmd<span class="w"> </span>mcp
</code></pre></div></div>

<p>Po restartu Claude Code session jsou dostupné nástroje: <code>qmd_search</code>, <code>qmd_vsearch</code>, <code>qmd_query</code>, <code>qmd_get</code>, <code>qmd_multi_get</code>.</p>
<hr>
<h2 id="workflow">🚀 Workflow</h2>
<h3 id="ingest">💼 Ingest</h3>
<p>Přidáš soubor do <code>raw/</code> (ručně, přes Obsidian Web Clipper nebo jinak) a řekneš agentovi:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">code</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code>Ingestuj raw/[soubor nebo složka]
</code></pre></div></div>

<p>Agent přečte zdroj, vytvoří nebo aktualizuje <code>wiki/synthesis/[téma].md</code>, aktualizuje <code>wiki/index.md</code> a zapíše do <code>wiki/log.md</code>.</p>
<h3 id="query">🔍 Query</h3>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">code</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code>Odpověz na: [otázka]
</code></pre></div></div>

<p>Agent prohledá <code>wiki/synthesis/</code> přes qmd. Pokud téma nenajde, prohledá <code>raw/</code> a vytvoří <code>wiki/query/[slug].md</code> se syntézou a wikilinky na zdrojové soubory.</p>
<h3 id="lint">🧹 Lint</h3>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">code</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code>Lint wiki
</code></pre></div></div>

<p>Agent zkontroluje synthesis stránky bez zdrojových odkazů, zastaralé stránky a chybějící témata. Výsledek zapíše do <code>wiki/query/lint-[datum].md</code>.</p>
<hr>
<h2 id="specifika-qubesos">📦 Specifika QubesOS</h2>
<h3 id="aktualizace-obsidian-na-qubesos">Aktualizace Obsidian na QubesOS</h3>
<p><strong>Systémový .deb balíček se neaktualizuje</strong> přes standardní <code>apt</code>, protože na QubesOS je rootfs v AppVM neperzistentní přes restarty (záleží na typu VM).</p>
<p><strong>App verze 1.12.7 se aktualizovala sama</strong>, protože:
- Zápis do <code>~/.config/</code> v home adresáři je povolen (home je persistentní i na QubesOS AppVM)
- Obsidian auto-updater fungoval v user-space, bez potřeby root</p>
<h4 id="prakticky-dopad">Praktický dopad</h4>
<ul>
<li>Dokud Electron shell v <code>/opt/</code> funguje a je kompatibilní, nemusíš .deb aktualizovat</li>
<li>Pokud by Obsidian vyžadoval novější Electron (např. kvůli novým API), pak by <code>installer version</code> zaostala a mohly by být problémy — ale to se u 1.7.5 → 1.12.7 zatím nestalo</li>
</ul>
<h4 id="dve-verze-dve-ruzne-veci">Dvě verze = dvě různé věci</h4>
<table>
<thead>
<tr>
<th>Co</th>
<th>Verze</th>
<th>Kde</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Installer version</strong></td>
<td>1.7.5</td>
<td><code>/opt/Obsidian/obsidian</code> — systémový .deb balíček</td>
</tr>
<tr>
<td><strong>App version</strong></td>
<td>1.12.7</td>
<td><code>~/.config/obsidian/</code> — auto-aktualizovaný app bundle</td>
</tr>
</tbody>
</table>
<h4 id="jak-to-funguje">Jak to funguje</h4>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">code</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code>/opt/Obsidian/obsidian
       │
       │  (Electron shell / launcher)
       ▼
~/.config/obsidian/obsidian-1.12.7.asar   ← skutečná aplikace
</code></pre></div></div>

<ol>
<li><code>.deb</code> balíček nainstaloval <strong>Electron runtime</strong> (shell) do <code>/opt/Obsidian/</code></li>
<li>Při prvním spuštění si Obsidian stáhl aktuální verzi aplikace jako <code>.asar</code> bundle do <strong>home adresáře uživatele</strong></li>
<li>Electron launcher vždy načte <strong>nejnovější <code>.asar</code></strong> z <code>~/.config/obsidian/</code></li>
</ol>
<p>Lze ověřit:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">bash</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code>ls<span class="w"> </span>-la<span class="w"> </span>~/.config/obsidian/
<span class="c1"># nebo</span>
find<span class="w"> </span>~/.config/obsidian<span class="w"> </span>-name<span class="w"> </span><span class="s2">&quot;*.asar&quot;</span><span class="w"> </span><span class="m">2</span>&gt;/dev/null
</code></pre></div></div>

<h4 id="proc-to-tak-delaji">Proč to tak dělají</h4>
<p>Electron shell se mění zřídka (nová verze Chromia/Node.js), zatímco samotná aplikace se aktualizuje často. Tím pádem nepotřebují root práva pro každou aktualizaci — <code>.asar</code> se aktualizuje pouze v home adresáři.</p>
<h3 id="vault-synchronizace">Vault synchronizace</h3>
<ul>
<li>Obsidian vault primárně žije v <code>notes VM</code> (Ubuntu 22.04 LTS) kde běží Obsidian.</li>
<li>Claude Code běží v oddělené <code>ai-anthropic VM</code> (Debian 13 Trixie).</li>
</ul>
<p>Synchronizaci mezi VM zajišťuje <strong>Syncthing</strong> — obě VM sledují stejnou složku vaultu.</p>
<p>Když přidáš zdroj do <code>raw/</code> v <code>notes VM</code> nebo AI zapíše synthesis stránku do <code>wiki/</code> v <code>ai-anthropic VM</code>, Syncthing změny automaticky propaguje na opačnou stranu. Není potřeba nic kopírovat ručně.</p>
<p>Syncthing běží v obou VM jako systemd user unit:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">bash</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code>systemctl<span class="w"> </span>--user<span class="w"> </span>status<span class="w"> </span>syncthing.service
systemctl<span class="w"> </span>--user<span class="w"> </span><span class="nb">enable</span><span class="w"> </span>syncthing.service<span class="w">  </span><span class="c1"># spuštění při startu VM</span>
</code></pre></div></div>

<p>Skills v <code>.claude/skills/</code> a <code>.agents/skills/</code> jsou součástí vaultu a tedy taky synchronizované — nejsou potřeba žádné globální instalace v agent VM.</p>
<p><code>qmd</code> a <code>bun</code> jsou nainstalované v <code>ai-anthropic</code> VM. Databáze qmd kolekcí zůstává lokálně v <code>ai-anthropic</code> VM (není součástí vaultu a nesynchronizuje se).</p>
<p><code>.stfolder</code> je Syncthing marker — musí zůstat na kořeni sledované složky (<code>~/Obsidian.SyncThing/</code>), ne v <code>raw/</code>. Při přesunu obsahu do <code>raw/</code> ho vynech.</p>
<hr>
<h2 id="vysledny-stav">✔️ Výsledný stav</h2>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">code</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code>~/Obsidian.SyncThing/
├── raw/                    ← 1 078 .md souborů, 42 složek, ~500 MB
│   └── attachments/        ← obrázky a PDF
├── wiki/
│   ├── synthesis/          ← LLM-kompilovaná témata
│   ├── query/              ← archiv dotazů a odpovědí
│   ├── index.md
│   └── log.md
├── .claude/
│   ├── mcp.json            ← qmd MCP server config
│   └── skills/             ← obsidian-markdown, obsidian-cli, ...
├── .agents/
│   └── skills/             ← totéž pro Codex
├── .obsidian/
├── .stfolder
└── CLAUDE.md
</code></pre></div></div>

<p>qmd kolekce: <code>my-vault</code> (1 078 souborů) a <code>wiki</code>. BM25 index aktivní, vektorové embeddingy se doindexovávají (~300 MB modely).</p>
<hr>
<h2 id="gotchas-shrnuti">😬 Gotchas shrnutí</h2>
<ol>
<li><strong><code>find</code> bez vylučování</strong> přesune i skryté složky jako <code>.stfolder</code>, <code>.claude</code> — vyluč je explicitně</li>
<li><strong><code>qmd collection add</code></strong> — první argument je cesta, název je <code>--name</code> (ne pozicionální)</li>
<li><strong>Kolekce s <code>&amp;&amp;</code></strong> — pokud první selže, zanechá prázdný záznam; přidávej každou zvlášť</li>
<li><strong>bun v PATH</strong> — qmd wrapper ho vyžaduje; v MCP env ho nastav explicitně</li>
<li><strong>MCP config</strong> patří do <code>.claude/mcp.json</code>, ne do <code>settings.json</code></li>
<li><strong><code>qmd embed</code></strong> je separátní krok od <code>qmd update</code>; bez něj funguje jen BM25 search</li>
<li><strong><code>wiki/ jako podsložka raw/</code></strong> je funkční alternativa bez přesunu obsahu, ale vyžaduje výjimky v každém promptu — <code>raw/</code> jako explicitní složka je čistší</li>
</ol>
    ]]></content>
    <author>
      <name>ruza</name>
    </author>
  </entry>
  <entry>
    <title>Windows - restart Voicemeeter po připojení Bluetooth zařízení</title>
    <link href="https://ruza.eu/articles/windows-restart-audio-on-bt-event.html" rel="alternate" type="text/html"/>
    <id>https://ruza.eu/articles/windows-restart-audio-on-bt-event.html</id>
    <published>2026-05-11T00:00:00Z</published>
    <updated>2026-05-11T00:00:00Z</updated>
    <summary>Automatický restart Voicemeeter po připojení Bluetooth zařízení</summary>
    <content type="html"><![CDATA[
<p>Tato stránka popisuje, jak na Windows 11 automaticky restartovat <strong>Voicemeeter Potato</strong> po připojení konkrétního Bluetooth zařízení.</p>
<blockquote>
<p><strong>Aktualizace (květen 2026):</strong> Původní postup z dubna 2026 přestal fungovat po Windows Update. Při ladění se ukázaly dvě příčiny — Windows Update tiše deaktivuje Bluetooth event log, a novější verze Voicemeeteru ignorují parametr <code>-r</code> způsobem, který původní skript předpokládal. Postup byl upraven tak, aby byl odolnější vůči oběma problémům. Změny jsou popsány v každém dotčeném kroku.</p>
</blockquote>
<h2 id="predpoklady">Předpoklady</h2>
<ul>
<li>Windows 11 s povoleným Bluetooth</li>
<li>Voicemeeter Potato nainstalovaný v <code>C:\Program Files (x86)\VB\Voicemeeter\</code></li>
<li>Povolený log <code>Microsoft-Windows-Bluetooth-Policy/Operational</code> v Event Vieweru</li>
</ul>
<h2 id="1-povoleni-bluetooth-event-logu">1. Povolení Bluetooth Event Logu</h2>
<p>Ve výchozím stavu je log vypnutý. Spusť <strong>PowerShell jako Administrator</strong>:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">powershell</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="n">wevtutil</span> <span class="nb">sl </span><span class="n">Microsoft-Windows-Bluetooth-Policy</span><span class="p">/</span><span class="n">Operational</span> <span class="p">/</span><span class="n">e</span><span class="p">:</span><span class="n">true</span>
</code></pre></div></div>

<p>Ověření:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">powershell</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="n">wevtutil</span> <span class="nb">gl </span><span class="s2">&quot;Microsoft-Windows-Bluetooth-Policy/Operational&quot;</span>
</code></pre></div></div>

<p>V výstupu hledej <code>enabled: true</code>.</p>
<h3 id="1a-zabraneni-opetovneho-vypnuti-logu-po-windows-update-novy-krok">1a. Zabránění opětovného vypnutí logu po Windows Update <em>(nový krok)</em></h3>
<p>Windows Update tento log po aktualizaci tiše deaktivuje — to byl hlavní důvod, proč původní postup přestal fungovat. Aby se to neopakovalo, vytvoř startup úlohu, která log při každém startu systému znovu zapne:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">powershell</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="nv">$action</span>    <span class="p">=</span> <span class="nb">New-ScheduledTaskAction</span> <span class="n">-Execute</span> <span class="s2">&quot;wevtutil.exe&quot;</span> <span class="n">-Argument</span> <span class="s1">&#39;sl &quot;Microsoft-Windows-Bluetooth-Policy/Operational&quot; /e:true&#39;</span>
<span class="nv">$trigger</span>   <span class="p">=</span> <span class="nb">New-ScheduledTaskTrigger</span> <span class="n">-AtStartup</span>
<span class="nv">$principal</span> <span class="p">=</span> <span class="nb">New-ScheduledTaskPrincipal</span> <span class="n">-UserId</span> <span class="s2">&quot;SYSTEM&quot;</span> <span class="n">-LogonType</span> <span class="n">ServiceAccount</span> <span class="n">-RunLevel</span> <span class="n">Highest</span>
<span class="nv">$settings</span>  <span class="p">=</span> <span class="nb">New-ScheduledTaskSettingsSet</span> <span class="n">-ExecutionTimeLimit</span> <span class="p">(</span><span class="nb">New-TimeSpan</span> <span class="n">-Minutes</span> <span class="n">1</span><span class="p">)</span>
<span class="nb">Register-ScheduledTask</span> <span class="n">-TaskName</span> <span class="s2">&quot;Enable-BT-Log&quot;</span> <span class="n">-Action</span> <span class="nv">$action</span> <span class="n">-Trigger</span> <span class="nv">$trigger</span> <span class="n">-Principal</span> <span class="nv">$principal</span> <span class="n">-Settings</span> <span class="nv">$settings</span> <span class="n">-Description</span> <span class="s2">&quot;Zapne Bluetooth event log po startu - potrebne pro automaticky restart Voicemeeteru&quot;</span>
</code></pre></div></div>

<p>Úloha se spustí jako SYSTEM s administrátorskými právy, takže nevyžaduje přihlášeného uživatele.</p>
<h2 id="2-zjisteni-adresy-bluetooth-zarizeni">2. Zjištění adresy Bluetooth zařízení</h2>
<p>Připoj cílové BT zařízení a spusť:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">powershell</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="nb">Get-WinEvent</span> <span class="n">-LogName</span> <span class="s2">&quot;Microsoft-Windows-Bluetooth-Policy/Operational&quot;</span> <span class="n">-MaxEvents</span> <span class="n">10</span> <span class="p">|</span>
  <span class="nb">Format-Table</span> <span class="n">TimeCreated</span><span class="p">,</span> <span class="n">Id</span><span class="p">,</span> <span class="n">Message</span> <span class="n">-Wrap</span>
</code></pre></div></div>

<p>Hledej řádek s <strong>Event ID 9</strong> (úspěšné připojení) a poznamenej si adresu zařízení (např. <code>A00CE238FE34</code>).</p>
<h2 id="3-vytvoreni-spousteciho-skriptu">3. Vytvoření spouštěcího skriptu</h2>
<p>Vytvoř soubor <code>C:\Scripts\voicemeeter-bt.bat</code>a ihned nastav správná oprávnění na složku — Task Scheduler skript spouští automaticky, takže zápis do <code>C:\Scripts\</code> by měl mít jen administrátor:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">batch</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="p">@</span><span class="k">echo</span> off
timeout /t 3 /nobreak <span class="p">&gt;</span>nul
taskkill /F /IM voicemeeter8x64.exe /T <span class="mi">2</span><span class="p">&gt;</span>nul
taskkill /F /IM VoicemeeterMacroButtons.exe /T <span class="mi">2</span><span class="p">&gt;</span>nul
timeout /t 1 /nobreak <span class="p">&gt;</span>nul
<span class="k">start</span> <span class="s2">&quot;&quot;</span> <span class="s2">&quot;C:\Program Files (x86)\VB\Voicemeeter\voicemeeter8x64.exe&quot;</span>
</code></pre></div></div>

<blockquote>
<p><strong>Změna oproti původnímu postupu:</strong> Původní skript spouštěl Voicemeeter s parametrem <code>-r</code>, který dříve fungoval správně — nová instance poslala existujícímu Voicemeeteru příkaz k restartu audio enginu a sama se ukončila. Někdy po dubnu 2026 toto chování přestalo fungovat (pravděpodobně aktualizací Voicemeeteru) — parametr <code>-r</code> nově otevírá druhou plnou instanci, která zůstane běžet. Skript proto nyní nejprve všechny instance ukončí (<code>taskkill</code>) a pak spustí čistou novou.</p>
</blockquote>
<p><code>timeout /t 3</code> na začátku dává BT zařízení čas se plně inicializovat před tím, než Voicemeeter nastartuje.</p>
<p>Nastav oprávnění složky (spusť jako Administrator):</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">powershell</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="n">icacls</span> <span class="s2">&quot;C:\Scripts&quot;</span> <span class="p">/</span><span class="n">inheritance</span><span class="p">:</span><span class="nb">r </span><span class="p">/</span><span class="n">grant</span><span class="p">:</span><span class="nb">r </span><span class="s2">&quot;BUILTIN\Administrators:(OI)(CI)(F)&quot;</span> <span class="s2">&quot;NT AUTHORITY\SYSTEM:(OI)(CI)(F)&quot;</span> <span class="s2">&quot;BUILTIN\Users:(OI)(CI)(RX)&quot;</span>
</code></pre></div></div>

<h2 id="4-nastaveni-ulohy-v-task-scheduleru">4. Nastavení úlohy v Task Scheduleru</h2>
<h3 id="41-otevreni-task-scheduleru">4.1 Otevření Task Scheduleru</h3>
<ul>
<li><code>Win+R</code> → <code>taskschd.msc</code> → Enter</li>
</ul>
<h3 id="42-vytvoreni-ulohy">4.2 Vytvoření úlohy</h3>
<ul>
<li>V pravém panelu klikni na <strong>Create Task</strong> (ne &ldquo;Create Basic Task&rdquo;)</li>
</ul>
<h3 id="43-zalozka-general">4.3 Záložka General</h3>
<ul>
<li><strong>Name:</strong> <code>Voicemeeter BT Auto-Start</code></li>
<li><strong>Description:</strong> <code>Spustí Voicemeeter Potato po připojení BT zařízení</code></li>
<li><strong>Security options:</strong> zaškrtni <strong>Run only when user is logged on</strong></li>
</ul>
<h3 id="44-zalozka-triggers">4.4 Záložka Triggers</h3>
<ol>
<li>Klikni <strong>New…</strong></li>
<li><strong>Begin the task:</strong> <code>On an event</code></li>
<li>Přepni na <strong>Custom</strong></li>
<li>Klikni <strong>New Event Filter…</strong> → záložka <strong>XML</strong> → zaškrtni <strong>Edit query manually</strong></li>
<li>Vlož následující XML (uprav <code>A00CE238FE34</code> na adresu svého zařízení):</li>
</ol>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">xml</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="nt">&lt;QueryList&gt;</span>
<span class="w">  </span><span class="nt">&lt;Query</span><span class="w"> </span><span class="na">Id=</span><span class="s">&quot;0&quot;</span><span class="w"> </span><span class="na">Path=</span><span class="s">&quot;Microsoft-Windows-Bluetooth-Policy/Operational&quot;</span><span class="nt">&gt;</span>
<span class="w">    </span><span class="nt">&lt;Select</span><span class="w"> </span><span class="na">Path=</span><span class="s">&quot;Microsoft-Windows-Bluetooth-Policy/Operational&quot;</span><span class="nt">&gt;</span>
<span class="w">      </span>*[System[EventID=9]<span class="w"> </span>and<span class="w"> </span>EventData[Data<span class="w"> </span>and<span class="w"> </span>contains(.,&#39;A00CE238FE34&#39;)]]
<span class="w">    </span><span class="nt">&lt;/Select&gt;</span>
<span class="w">  </span><span class="nt">&lt;/Query&gt;</span>
<span class="nt">&lt;/QueryList&gt;</span>
</code></pre></div></div>

<ol start="6">
<li>Potvrď <strong>OK</strong></li>
</ol>
<h3 id="45-zalozka-actions">4.5 Záložka Actions</h3>
<ol>
<li>Klikni <strong>New…</strong></li>
<li><strong>Action:</strong> <code>Start a program</code></li>
<li><strong>Program/script:</strong> <code>C:\Scripts\voicemeeter-bt.bat</code></li>
<li>Potvrď <strong>OK</strong></li>
</ol>
<h3 id="46-zalozka-conditions">4.6 Záložka Conditions</h3>
<ul>
<li><strong>Odškrtni</strong> &ldquo;Start the task only if the computer is on AC power&rdquo; (pokud chceš spouštět i na baterii)</li>
</ul>
<h3 id="47-zalozka-settings">4.7 Záložka Settings</h3>
<ul>
<li>Zaškrtni <strong>Allow task to be run on demand</strong> (pro ruční testování)</li>
<li>Zaškrtni <strong>If the task is already running, then do not start a new instance</strong></li>
<li>Potvrď <strong>OK</strong></li>
</ul>
<h2 id="5-testovani">5. Testování</h2>
<ol>
<li>Odpoj BT zařízení</li>
<li>Znovu ho připoj</li>
<li>Po 2 sekundách by se měl spustit Voicemeeter Potato</li>
</ol>
<p>Pro diagnostiku zkontroluj:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">powershell</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="nb">Get-WinEvent</span> <span class="n">-LogName</span> <span class="s2">&quot;Microsoft-Windows-Bluetooth-Policy/Operational&quot;</span> <span class="n">-MaxEvents</span> <span class="n">5</span> <span class="p">|</span>
  <span class="nb">Format-Table</span> <span class="n">TimeCreated</span><span class="p">,</span> <span class="n">Id</span><span class="p">,</span> <span class="n">Message</span> <span class="n">-Wrap</span>
</code></pre></div></div>

<p>Historii spouštění úlohy najdeš v Task Scheduleru pod <strong>Task Scheduler Library</strong> → pravý klik na úlohu → <strong>Properties</strong> → záložka <strong>History</strong>.</p>
<h2 id="reseni-problemu">Řešení problémů</h2>
<table>
<thead>
<tr>
<th>Problém</th>
<th>Řešení</th>
</tr>
</thead>
<tbody>
<tr>
<td>Postup přestal fungovat po Windows Update</td>
<td>Windows Update deaktivuje Bluetooth event log. Ověř stav: <code>wevtutil gl "Microsoft-Windows-Bluetooth-Policy/Operational"</code> — hledej <code>enabled: true</code>. Pokud je <code>false</code>, znovu zapni (krok 1) a ujisti se, že máš vytvořenou startup úlohu <code>Enable-BT-Log</code> (krok 1a).</td>
</tr>
<tr>
<td>Log je prázdný</td>
<td>Ověř, že je log povolený (krok 1)</td>
</tr>
<tr>
<td>Event ID 9 se neobjevuje</td>
<td>Některé BT adaptéry logují jinak – zkontroluj i logy <code>Bluetooth-BthMini</code> a <code>System</code></td>
</tr>
<tr>
<td>Úloha se nespouští</td>
<td>Zkontroluj, že XML query odpovídá přesné adrese zařízení; otestuj úlohu ručně přes pravý klik → Run</td>
</tr>
<tr>
<td>Voicemeeter se spustí vícekrát</td>
<td>Ujisti se, že používáš aktuální verzi skriptu z kroku 3 (s <code>taskkill</code>). V Settings úlohy nastav také &ldquo;Do not start a new instance&rdquo;.</td>
</tr>
</tbody>
</table>
<h2 id="poznamky">Poznámky</h2>
<ul>
<li>Pokud chceš reagovat na <strong>jakékoli</strong> BT zařízení (ne jen jedno), odstraň z XML podmínku <code>and EventData[...]</code> a ponechej pouze <code>*[System[EventID=9]]</code>.</li>
<li>Skript ukončuje i <code>VoicemeeterMacroButtons.exe</code> — pokud Macro Buttons nepoužíváš, řádek s ním můžeš vynechat. Pokud ho používáš, po restartu se spustí automaticky jako závislý proces Voicemeeteru.</li>
<li>Po aktualizaci Windows ověř, že je log stále aktivní (<code>wevtutil gl ...</code>) a že startup úloha <code>Enable-BT-Log</code> existuje a je ve stavu <code>Ready</code>.</li>
</ul>
    ]]></content>
    <author>
      <name>ruza</name>
    </author>
  </entry>
  <entry>
    <title>QubesOS - DisposableVM s VPN per zákazník</title>
    <link href="https://ruza.eu/articles/qubesos-named-disposables.html" rel="alternate" type="text/html"/>
    <id>https://ruza.eu/articles/qubesos-named-disposables.html</id>
    <published>2026-04-18T00:00:00Z</published>
    <updated>2026-04-18T00:00:00Z</updated>
    <summary>Návod na zprovoznění DisposableVM s VPN ke konkrétním zákazníkům</summary>
    <content type="html"><![CDATA[
<h1 id="zadani">Zadání</h1>
<p>Cílem je z menu mít relativně rychle dostupné jednotné prostředí s předpřipraveným VPN přístupem k různým zákazníkům.</p>
<ul>
<li>Jedna AppVM <code>work</code>, která drží persistentní konfiguraci v /home/ adresáři.</li>
<li>pro každého zákazníka<ul>
<li><code>work-zákazníkX</code> disposable pro práci u konkrétního zákazníka</li>
<li><code>vpn-zákazníkX</code> s VPN klientem k zákazníkX</li>
</ul>
</li>
</ul>
<h2 id="architektura">Architektura</h2>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">code</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code>[work-zákazník1] ──► [vpn-zákazník1] ──► [sys-firewall] ──► [sys-net]
[work-zákazník2] ──► [vpn-zákazník2] ──► [sys-firewall] ──► [sys-net]
</code></pre></div></div>

<p>Každý Named DispVM se při startu vytvoří čistý z šablony, má vlastní VPN tunel a po zavření se smaže.</p>
<hr>
<h2 id="1-priprav-base-appvm-jako-dispvm-sablonu">1. Připrav base AppVM jako DispVM šablonu</h2>
<p>Máš AppVM (řekněme <code>work</code>), kde máš nainstalované SSH klienty, nástroje atd. Tu nastavíš jako šablonu pro disposable:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">bash</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="c1"># v dom0</span>
qvm-prefs<span class="w"> </span>work<span class="w"> </span>template_for_dispvms<span class="w"> </span>True
</code></pre></div></div>

<p>AppVM  <code>work</code> můžeš normálně používat a pokud budeš potřebovat nové work prostředí s VPN ke konkrétnímu zákazníkovi spustíš odpovídající work disposable.</p>
<p>Networking u <code>work</code> samotné můžeš nastavit na <code>none</code>, pokud budeš používat jen disposable. Síť se řeší až na úrovni Named DispVMs:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">bash</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code>qvm-prefs<span class="w"> </span>work<span class="w"> </span>netvm<span class="w"> </span>none
</code></pre></div></div>

<p>Veškerý software (SSH klient, wireshark, nmap…) instaluješ do TemplateVM, ze které <code>work</code> vychází (např. <code>debian-13</code>). Samotná <code>work</code> AppVM pak slouží jako DispVM šablona — přizpůsobení (dotfiles, SSH config) dáváš do <code>/home/user/</code> v <code>work</code>.</p>
<p>Nezapomeň, že v disposable se nic persistentně na disk neukládá.</p>
<hr>
<h2 id="2-vytvor-vpn-proxyvm-pro-kazdeho-zakaznika">2. Vytvoř VPN ProxyVM pro každého zákazníka</h2>
<p>Pro každého zákazníka potřebuješ samostatnou VM, která poběží jako VPN gateway.</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">bash</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="c1"># v dom0</span>
qvm-create<span class="w"> </span>vpn-zákazník1<span class="w"> </span>--class<span class="w"> </span>AppVM<span class="w"> </span>--template<span class="w"> </span>debian-13<span class="w"> </span>--label<span class="w"> </span>orange
qvm-prefs<span class="w"> </span>vpn-zákazník1<span class="w"> </span>netvm<span class="w"> </span>sys-firewall
qvm-prefs<span class="w"> </span>vpn-zákazník1<span class="w"> </span>provides_network<span class="w"> </span>True

qvm-create<span class="w"> </span>vpn-zákazník2<span class="w"> </span>--class<span class="w"> </span>AppVM<span class="w"> </span>--template<span class="w"> </span>debian-13<span class="w"> </span>--label<span class="w"> </span>orange
qvm-prefs<span class="w"> </span>vpn-zákazník2<span class="w"> </span>netvm<span class="w"> </span>sys-firewall
qvm-prefs<span class="w"> </span>vpn-zákazník2<span class="w"> </span>provides_network<span class="w"> </span>True
</code></pre></div></div>

<p><code>provides_network True</code> je klíčové — díky tomu se VM chová jako ProxyVM a ostatní VM ji můžou použít jako svůj <code>netvm</code>.</p>
<h3 id="konfigurace-vpn-uvnitr-kazde-proxy-vm">Konfigurace VPN uvnitř každé proxy VM</h3>
<p>Spusť <code>vpn-zákazník1</code> a nastav VPN. Příklad s WireGuard:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">bash</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="c1"># uvnitř vpn-zákazník1</span>
sudo<span class="w"> </span>nano<span class="w"> </span>/rw/config/vpn/wg0.conf
<span class="c1"># vlož WireGuard config zákazníka 1</span>
</code></pre></div></div>

<p>Aby se VPN spustila automaticky při startu, přidej do <code>/rw/config/rc.local</code>:</p>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">bash</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="ch">#!/bin/bash</span>
cp<span class="w"> </span>/rw/config/vpn/wg0.conf<span class="w"> </span>/etc/wireguard/wg0.conf
systemctl<span class="w"> </span>start<span class="w"> </span>wg-quick@wg0

<span class="c1"># Povolit forwarding (nutné pro ProxyVM funkci)</span>
<span class="nb">echo</span><span class="w"> </span><span class="m">1</span><span class="w"> </span>&gt;<span class="w"> </span>/proc/sys/net/ipv4/ip_forward
iptables<span class="w"> </span>-t<span class="w"> </span>nat<span class="w"> </span>-A<span class="w"> </span>POSTROUTING<span class="w"> </span>-o<span class="w"> </span>wg0<span class="w"> </span>-j<span class="w"> </span>MASQUERADE
iptables<span class="w"> </span>-A<span class="w"> </span>FORWARD<span class="w"> </span>-i<span class="w"> </span>eth0<span class="w"> </span>-o<span class="w"> </span>wg0<span class="w"> </span>-j<span class="w"> </span>ACCEPT
iptables<span class="w"> </span>-A<span class="w"> </span>FORWARD<span class="w"> </span>-i<span class="w"> </span>wg0<span class="w"> </span>-o<span class="w"> </span>eth0<span class="w"> </span>-m<span class="w"> </span>state<span class="w"> </span>--state<span class="w"> </span>RELATED,ESTABLISHED<span class="w"> </span>-j<span class="w"> </span>ACCEPT

<span class="c1"># Volitelně: blokuj traffic mimo tunel (kill switch)</span>
iptables<span class="w"> </span>-A<span class="w"> </span>OUTPUT<span class="w"> </span>-o<span class="w"> </span>wg0<span class="w"> </span>-j<span class="w"> </span>ACCEPT
iptables<span class="w"> </span>-A<span class="w"> </span>OUTPUT<span class="w"> </span>-o<span class="w"> </span>lo<span class="w"> </span>-j<span class="w"> </span>ACCEPT
iptables<span class="w"> </span>-A<span class="w"> </span>OUTPUT<span class="w"> </span>-d<span class="w"> </span><span class="m">10</span>.137.0.0/16<span class="w"> </span>-j<span class="w"> </span>ACCEPT<span class="w">  </span><span class="c1"># Qubes interní síť</span>
iptables<span class="w"> </span>-A<span class="w"> </span>OUTPUT<span class="w"> </span>-p<span class="w"> </span>udp<span class="w"> </span>--dport<span class="w"> </span><span class="m">51820</span><span class="w"> </span>-j<span class="w"> </span>ACCEPT<span class="w">  </span><span class="c1"># WG endpoint</span>
iptables<span class="w"> </span>-A<span class="w"> </span>OUTPUT<span class="w"> </span>-j<span class="w"> </span>DROP
</code></pre></div></div>

<p>Pro OpenVPN je postup analogický — místo WireGuard spustíš <code>openvpn --config /rw/config/vpn/client.ovpn --daemon</code>.</p>
<p>Totéž provedeš pro <code>vpn-zákazník2</code> s konfigurací druhého zákazníka.</p>
<hr>
<h2 id="3-vytvor-named-disposables">3. Vytvoř Named Disposables</h2>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">bash</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="c1"># v dom0</span>
qvm-create<span class="w"> </span>work-zákazník1<span class="w"> </span>--class<span class="w"> </span>DispVM<span class="w"> </span>--template<span class="w"> </span>work<span class="w"> </span>--label<span class="w"> </span>green
qvm-prefs<span class="w"> </span>work-zákazník1<span class="w"> </span>netvm<span class="w"> </span>vpn-zákazník1

qvm-create<span class="w"> </span>work-zákazník2<span class="w"> </span>--class<span class="w"> </span>DispVM<span class="w"> </span>--template<span class="w"> </span>work<span class="w"> </span>--label<span class="w"> </span>blue
qvm-prefs<span class="w"> </span>work-zákazník2<span class="w"> </span>netvm<span class="w"> </span>vpn-zákazník2
</code></pre></div></div>

<p>Tím vzniknou pojmenované DispVM, které se zobrazí v App menu a můžeš je spouštět opakovaně, pokaždé čisté, ale vždy s přiřazenou VPN.</p>
<h2 id="vytvoreni-skriptem">Vytvoření skriptem</h2>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">bash</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="ch">#!/bin/bash                                                                                                                           </span>
<span class="nv">ZAKAZNIK</span><span class="o">=</span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">1</span><span class="si">}</span><span class="s2">&quot;</span>

<span class="c1"># Kontrola, zda je promenna ZAKAZNIK nastavena                                                                                        </span>
<span class="k">if</span><span class="w"> </span><span class="o">[</span><span class="w"> </span>-z<span class="w"> </span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">ZAKAZNIK</span><span class="si">}</span><span class="s2">&quot;</span><span class="w"> </span><span class="o">]</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
<span class="w">    </span><span class="c1"># Pokud neni nastavena, zkontroluj prvni parametr                                                                                 </span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="o">[</span><span class="w"> </span><span class="nv">$#</span><span class="w"> </span>-eq<span class="w"> </span><span class="m">0</span><span class="w"> </span><span class="o">]</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="o">[</span><span class="w"> </span>-z<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$1</span><span class="s2">&quot;</span><span class="w"> </span><span class="o">]</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
<span class="w">        </span><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;Chyba: Promenna ZAKAZNIK neni nastavena a nebyl poskytnut prvni parametr.&quot;</span>
<span class="w">        </span><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;Zadejte hodnotu pro ZAKAZNIK:&quot;</span>
<span class="w">        </span><span class="nb">read</span><span class="w"> </span>-r<span class="w"> </span>ZAKAZNIK
<span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="o">[</span><span class="w"> </span>-z<span class="w"> </span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">ZAKAZNIK</span><span class="si">}</span><span class="s2">&quot;</span><span class="w"> </span><span class="o">]</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
<span class="w">            </span><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;Chyba: ZAKAZNIK neni zadan. Skript konci.&quot;</span>
<span class="w">            </span><span class="nb">exit</span><span class="w"> </span><span class="m">1</span>
<span class="w">        </span><span class="k">fi</span>
<span class="w">    </span><span class="k">else</span>
<span class="w">        </span><span class="nv">ZAKAZNIK</span><span class="o">=</span><span class="s2">&quot;</span><span class="nv">$1</span><span class="s2">&quot;</span>
<span class="w">    </span><span class="k">fi</span>
<span class="k">fi</span>

<span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;Vytvorim: work-</span><span class="si">${</span><span class="nv">ZAKAZNIK</span><span class="si">}</span><span class="s2"> jako NamedDispVM..&quot;</span>

qvm-create<span class="w"> </span>work-<span class="si">${</span><span class="nv">ZAKAZNIK</span><span class="si">}</span><span class="w"> </span>--class<span class="w"> </span>DispVM<span class="w"> </span>--template<span class="w"> </span>work<span class="w"> </span>--label<span class="w"> </span>blue
qvm-prefs<span class="w"> </span>work-<span class="si">${</span><span class="nv">ZAKAZNIK</span><span class="si">}</span><span class="w"> </span>netvm<span class="w"> </span>vpn-<span class="si">${</span><span class="nv">ZAKAZNIK</span><span class="si">}</span>
</code></pre></div></div>

<h2 id="4-overeni">4. Ověření</h2>
<div class="code-block"><div class="code-block-header"><span class="code-block-lang">bash</span><button type="button" class="code-block-copy" aria-label="copy code">[ copy ]</button></div><div class="codehilite"><pre><span></span><code><span class="c1"># Spusť Named DispVM</span>
qvm-run<span class="w"> </span>work-zákazník1<span class="w"> </span>--auto<span class="w"> </span>gnome-terminal

<span class="c1"># Uvnitř DispVM ověř IP</span>
curl<span class="w"> </span>ifconfig.me
<span class="c1"># Měla by se zobrazit VPN IP zákazníka 1</span>
</code></pre></div></div>

<hr>
<h2 id="shrnuti-struktury">Shrnutí struktury</h2>
<table>
<thead>
<tr>
<th>VM</th>
<th>Třída</th>
<th>NetVM</th>
<th>Účel</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>work</code></td>
<td>AppVM a DispVM šablona</td>
<td>none / sys-firewall</td>
<td>Šablona s nástroji a dotfiles</td>
</tr>
<tr>
<td><code>vpn-zákazník1</code></td>
<td>AppVM (provides_network)</td>
<td>sys-firewall</td>
<td>VPN tunel zákazníka 1</td>
</tr>
<tr>
<td><code>vpn-zákazník2</code></td>
<td>AppVM (provides_network)</td>
<td>sys-firewall</td>
<td>VPN tunel zákazníka 2</td>
</tr>
<tr>
<td><code>work-zákazník1</code></td>
<td>DispVM</td>
<td>vpn-zákazník1</td>
<td>Pracovní session zákazník 1</td>
</tr>
<tr>
<td><code>work-zákazník2</code></td>
<td>DispVM</td>
<td>vpn-zákazník2</td>
<td>Pracovní session zákazník 2</td>
</tr>
</tbody>
</table>
<h2 id="tipy">Tipy</h2>
<p><strong>Přidání dalšího zákazníka</strong> je vždy jen 3 příkazy v dom0 — vytvoř VPN proxy, nakonfiguruj VPN uvnitř, vytvoř Named DispVM s příslušným netvm.</p>
<p><strong>SSH klíče</strong> — pokud používáš split-GPG/split-SSH, klíče zůstávají ve <code>vault-net</code> a DispVM si je vyžádá přes Qubes RPC. Nemusíš je kopírovat do každé disposable.</p>
<p><strong>Persistentní SSH config</strong> — <code>/home/user/.ssh/config</code> v <code>work</code> AppVM se propaguje do každého DispVM, takže aliasy, proxy jumpy atd. stačí nastavit jednou.</p>
    ]]></content>
    <author>
      <name>ruza</name>
    </author>
  </entry>
  <entry>
    <title>Nový design ruza.eu</title>
    <link href="https://ruza.eu/articles/novy-design.html" rel="alternate" type="text/html"/>
    <id>https://ruza.eu/articles/novy-design.html</id>
    <published>2026-04-17T00:00:00Z</published>
    <updated>2026-04-17T00:00:00Z</updated>
    <summary>Přepis starého osobního rozcestníku na nový terminálový design s Markdown workflow</summary>
    <content type="html"><![CDATA[
<p>Stará verze stránky byla prostá HTML tabulka — funkční, ale vizuálně z roku 2003. Po pár iteracích se zrodil nový design: černé pozadí, žluté odkazy, monospace typografie, Mastodon feed a jednotné <code>style.css</code>.</p>
<h2 id="co-se-zmenilo">Co se změnilo</h2>
<ul>
<li>dark mode s volitelným light mode přes <code>prefers-color-scheme</code></li>
<li>socialní odkazy v karetním gridu s pořádnými SVG ikonkami</li>
<li>GPG fingerprint s copy-to-clipboard funkcí</li>
<li>sekce <em>writing</em> s odkazy na články</li>
<li>tři nejnovější Mastodon posty načítané přes veřejné API</li>
<li>všechny stránky sdílí jediný <code>/style.css</code></li>
</ul>
<h2 id="markdown-pipeline">Markdown pipeline</h2>
<p>Místo psaní HTML ručně teď mám build script <code>build.py</code>, který bere Markdown soubor s YAML frontmatter a generuje finální HTML. Proces:</p>
<ol>
<li>Napíšu článek v Obsidianu jako <code>.md</code></li>
<li>Spustím <code>python3 build.py articles/src/</code></li>
<li>Hotovo — vygeneruje se jak jednotlivý článek, tak aktualizovaný seznam</li>
</ol>
<h2 id="citace">Citace</h2>
<blockquote>
<p>Dobré rozhraní je jako vtip. Když ho musíte vysvětlovat, není moc dobré.
— Martin LeBlanc</p>
</blockquote>
    ]]></content>
    <author>
      <name>ruza</name>
    </author>
  </entry>
</feed>