<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: sir'Alexander</title>
    <description>The latest articles on DEV Community by sir'Alexander (@sir_alexander_t).</description>
    <link>https://dev.to/sir_alexander_t</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F164660%2Fa798aaa5-258b-435f-bb18-26aa953763da.webp</url>
      <title>DEV Community: sir'Alexander</title>
      <link>https://dev.to/sir_alexander_t</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sir_alexander_t"/>
    <language>en</language>
    <item>
      <title>Why Your OpenClaw Telegram Bot Goes Silent</title>
      <dc:creator>sir'Alexander</dc:creator>
      <pubDate>Tue, 12 May 2026 09:21:48 +0000</pubDate>
      <link>https://dev.to/sir_alexander_t/why-your-openclaw-telegram-bot-goes-silent-50ag</link>
      <guid>https://dev.to/sir_alexander_t/why-your-openclaw-telegram-bot-goes-silent-50ag</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgld26pi2hr20z8fh6uqv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgld26pi2hr20z8fh6uqv.png" alt="Telegram chat with AI agent on a smartphone"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The silent failures, the two-switch approval trap, and the config gaps that kept adding sessions.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; The OpenClaw Telegram integration guide gives you five steps. They work. What they skip: &lt;code&gt;groupPolicy: "allowlist"&lt;/code&gt; is the right setting for group chat access, but it needs a companion — &lt;code&gt;groupAllowFrom&lt;/code&gt;, the actual sender allowlist. &lt;code&gt;groupAllowFrom&lt;/code&gt; takes your numeric Telegram user ID, which the app never shows you. If &lt;code&gt;groupAllowFrom&lt;/code&gt; is absent, OpenClaw falls back to &lt;code&gt;allowFrom&lt;/code&gt;. If that is also empty, nobody can invoke the agent in groups — silently, with no error. The exec approval system is the other trap: it has two independent layers. I disabled the first and was still getting prompts. The fix is two steps, not one. None of this is documented in the setup guide. Correct config is in the second section.&lt;/p&gt;




&lt;p&gt;I followed the Telegram integration guide for OpenClaw and connected the bot. Five steps, they worked. What the guide did not prepare me for: config keys with undocumented dependencies, an approval system with two independent layers that need to be disabled separately, and a config file that gives you near-zero diagnostic output when it breaks.&lt;/p&gt;

&lt;p&gt;Each one cost a session I had not planned for.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Five Steps
&lt;/h2&gt;

&lt;p&gt;The guide covers getting the bot connected. Here is the sequence.&lt;/p&gt;

&lt;p&gt;BotFather on Telegram — send &lt;code&gt;/newbot&lt;/code&gt;, give it a name and a username, and BotFather hands you a token. Add it to &lt;code&gt;~/.openclaw/openclaw.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"channels"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"telegram"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"botToken"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_BOT_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dmPolicy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pairing"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pair your account: send &lt;code&gt;/start&lt;/code&gt; to the bot in Telegram. Then approve the pairing from the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw pairing list telegram
openclaw pairing approve telegram &amp;lt;CODE&amp;gt; &lt;span class="nt"&gt;--notify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;&amp;lt;CODE&amp;gt;&lt;/code&gt; with the code shown by &lt;code&gt;pairing list&lt;/code&gt;. The &lt;code&gt;--notify&lt;/code&gt; flag sends a confirmation back to you in Telegram. Once approved, messages you send to the bot route to your agent.&lt;/p&gt;

&lt;p&gt;On the happy path — one user, direct messages, default config — this works. The gateway starts. The agent responds. The guide delivered.&lt;/p&gt;

&lt;p&gt;What the guide did not give you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;groupPolicy: "allowlist"&lt;/code&gt; — the default setting, but requires &lt;code&gt;groupAllowFrom&lt;/code&gt; (or &lt;code&gt;allowFrom&lt;/code&gt;) to have your user ID before it does anything&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;groupAllowFrom&lt;/code&gt; — the sender allowlist; takes a numeric user ID the app never shows you; if absent, falls back to &lt;code&gt;allowFrom&lt;/code&gt;; if both are empty, fails silently&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;execApprovals&lt;/code&gt; — two independent systems; disabling only the first leaves the second still prompting&lt;/li&gt;
&lt;li&gt;Config file integrity — the gateway fails to start with near-zero diagnostic output if &lt;code&gt;openclaw.json&lt;/code&gt; is structurally broken&lt;/li&gt;
&lt;li&gt;Schema migration — &lt;code&gt;openclaw doctor --fix&lt;/code&gt; is a migration tool you have to know to run&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each of those is a session I did not expect to spend.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2432zustbjo8uta782aw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2432zustbjo8uta782aw.png" alt="OpenClaw config file open in a code editor"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Correct Config — Before the Story
&lt;/h2&gt;

&lt;p&gt;If you just need the answer, here it is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In &lt;code&gt;~/.openclaw/openclaw.json&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"channels"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"telegram"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"botToken"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_BOT_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"groupPolicy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"allowlist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"groupAllowFrom"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"YOUR_NUMERIC_TELEGRAM_USER_ID"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"execApprovals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"streaming"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"partial"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;In &lt;code&gt;~/.openclaw/exec-approvals.json&lt;/code&gt;&lt;/strong&gt; (separate file — System 2):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"defaults"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"security"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"full"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ask"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"off"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"askFallback"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"full"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One sentence per key:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;botToken&lt;/code&gt;&lt;/strong&gt;: your BotFather token — required, you have this already&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;groupPolicy&lt;/code&gt;&lt;/strong&gt;: controls who can invoke the agent in group chats — &lt;code&gt;"allowlist"&lt;/code&gt; is correct; requires &lt;code&gt;groupAllowFrom&lt;/code&gt; or &lt;code&gt;allowFrom&lt;/code&gt; to contain your numeric user ID&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;groupAllowFrom&lt;/code&gt;&lt;/strong&gt;: the sender allowlist — your numeric Telegram user ID (not your @username); if absent, OpenClaw falls back to &lt;code&gt;allowFrom&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;execApprovals.enabled: false&lt;/code&gt;&lt;/strong&gt;: disables channel-level approval prompts — System 1 of two&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;defaults.security: "full"&lt;/code&gt; / &lt;code&gt;ask: "off"&lt;/code&gt; / &lt;code&gt;askFallback: "full"&lt;/code&gt;&lt;/strong&gt; in &lt;code&gt;exec-approvals.json&lt;/code&gt;: disables the host exec policy — System 2 of two&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;streaming.mode: "partial"&lt;/code&gt;&lt;/strong&gt;: object form; run &lt;code&gt;openclaw doctor --fix&lt;/code&gt; if you have the older &lt;code&gt;streaming: "partial"&lt;/code&gt; scalar&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is what each of those actually means.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Keys, and What Happens Without Them
&lt;/h2&gt;

&lt;p&gt;None of these are in the setup guide, the README, or the standard integration walkthrough. A fresh install will not warn you they are missing. You find them when something stops working — or when something is not working and you cannot tell why.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;groupPolicy&lt;/code&gt; and &lt;code&gt;groupAllowFrom&lt;/code&gt; — the policy needs a list&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;groupPolicy: "allowlist"&lt;/code&gt; is correct. It is also the default. What the guide does not explain is that this key needs a populated allowlist to do anything. &lt;code&gt;"allowlist"&lt;/code&gt; means: only users on the allowlist may invoke the agent in group chats. The allowlist is &lt;code&gt;groupAllowFrom&lt;/code&gt;. If &lt;code&gt;groupAllowFrom&lt;/code&gt; is absent, OpenClaw falls back to &lt;code&gt;allowFrom&lt;/code&gt; for group sender authorization. If neither contains your numeric user ID, nobody can invoke the agent in groups — including you — with no error and no log entry.&lt;/p&gt;

&lt;p&gt;The guide shows the lock. It does not mention the key.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;groupAllowFrom&lt;/code&gt; takes an array of numeric Telegram user IDs. Not @usernames — numeric IDs. A long integer Telegram assigns internally to every account. It does not appear in the app's profile screen, settings, or any menu.&lt;/p&gt;

&lt;p&gt;Two ways to get it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Via gateway logs&lt;/strong&gt; — send a message to your bot while running &lt;code&gt;openclaw logs --follow&lt;/code&gt;. The log output includes &lt;code&gt;from.id&lt;/code&gt; for each inbound message. That is your numeric ID. This works if your gateway is already running.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Via the Bot API&lt;/strong&gt; — works from any state, as long as the bot has received at least one message:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   curl &lt;span class="s2"&gt;"https://api.telegram.org/bot&amp;lt;YOUR_BOT_TOKEN&amp;gt;/getUpdates"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response includes &lt;code&gt;message.from.id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Once you have the number, put it in &lt;code&gt;groupAllowFrom&lt;/code&gt;. A wrong value fails silently — the agent stops responding in group chats with no error and no log entry. If &lt;code&gt;groupAllowFrom&lt;/code&gt; is absent, OpenClaw falls back to &lt;code&gt;allowFrom&lt;/code&gt;; if that is also missing or wrong, same silent failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;execApprovals&lt;/code&gt; — two switches, not one&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This was the one I expected to solve in a single config change. It took two.&lt;/p&gt;

&lt;p&gt;OpenClaw has two independent exec approval systems. Disabling only the first left the second still prompting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;System 1 — &lt;code&gt;channels.telegram.execApprovals&lt;/code&gt; in &lt;code&gt;openclaw.json&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Controls channel-level approval prompts — the ones triggered when you send a message via Telegram or the web UI. Auto-enables when approvers are resolvable.&lt;/p&gt;

&lt;p&gt;What that means in practice: the agent wants to open a browser — &lt;em&gt;Approve?&lt;/em&gt; Navigate to a URL — &lt;em&gt;Approve?&lt;/em&gt; Read the page — &lt;em&gt;Approve?&lt;/em&gt; Run a command — &lt;em&gt;Approve?&lt;/em&gt; Not once per task — once per step.&lt;/p&gt;

&lt;p&gt;Ask the agent to summarize a webpage: approve the browser open, approve the navigation, approve the DOM read, potentially approve a redirect. Four approvals for thirty seconds of actual work.&lt;/p&gt;

&lt;p&gt;Fix: set &lt;code&gt;execApprovals.enabled: false&lt;/code&gt; in &lt;code&gt;openclaw.json&lt;/code&gt; under &lt;code&gt;channels.telegram&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;System 2 — &lt;code&gt;~/.openclaw/exec-approvals.json&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A separate file-based policy system. Its own file, its own config, completely independent of &lt;code&gt;openclaw.json&lt;/code&gt;. When the file does not exist, the system defers to the &lt;code&gt;tools.exec.*&lt;/code&gt; settings in &lt;code&gt;openclaw.json&lt;/code&gt;. If those are also unset, the stricter of the two wins — which means you can still get prompted even after disabling System 1.&lt;/p&gt;

&lt;p&gt;For a single-user personal setup, set it to allow everything and ask nothing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"defaults"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"security"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"full"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ask"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"off"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"askFallback"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"full"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: &lt;code&gt;security: "full"&lt;/code&gt; is counterintuitive — it means all commands are permitted and the allowlist is bypassed entirely. It is the permissive setting, not the restrictive one. &lt;code&gt;askFallback: "full"&lt;/code&gt; covers the case where no UI is available; without it, the default is &lt;code&gt;deny&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If the file does not exist, create it at &lt;code&gt;~/.openclaw/exec-approvals.json&lt;/code&gt; with that content. If it already exists, update the &lt;code&gt;defaults&lt;/code&gt; block. Restart the gateway.&lt;/p&gt;

&lt;p&gt;YOLO mode also requires the gateway-side policy to match. The cleanest way to set both layers at once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw exec-policy preset yolo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or manually: &lt;code&gt;openclaw config set tools.exec.security full&lt;/code&gt; and &lt;code&gt;openclaw config set tools.exec.ask off&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After both layers are aligned: no approval prompts. If you ever open access to other users, re-enable both.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Sessions I Didn't Plan For
&lt;/h2&gt;

&lt;p&gt;Even with the config keys right, two incidents added sessions I hadn't budgeted for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The AI wrote a config line outside the config file&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Early in testing, the gateway entered a crash-loop that lasted three days. No response to messages. The logs offered almost nothing — the failure was happening before the gateway could initialise.&lt;/p&gt;

&lt;p&gt;Opening &lt;code&gt;~/.openclaw/openclaw.json&lt;/code&gt; showed the problem. A &lt;code&gt;groupAllowFrom&lt;/code&gt; value had been written as a bare property assignment after the closing &lt;code&gt;}&lt;/code&gt; of the JSON5 file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hocon"&gt;&lt;code&gt;&lt;span class="nl"&gt;channels.telegram.groupAllowFrom&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;user-id&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Invalid JSON5. The gateway cannot parse the file on startup, so it cannot start.&lt;/p&gt;

&lt;p&gt;Fix: delete everything after the final &lt;code&gt;}&lt;/code&gt;, re-add the value correctly inside the &lt;code&gt;channels.telegram&lt;/code&gt; block. Before restarting, run the doctor to confirm the config is valid:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw doctor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it reports no errors, restart the gateway.&lt;/p&gt;

&lt;p&gt;This is a different failure from the hallucinated config keys in my &lt;a href="https://medium.com/@alexander.shikanga.tindi/from-broken-docker-containers-to-a-working-ai-agent-the-full-openclaw-journey-e599ed5f5bdc" rel="noopener noreferrer"&gt;first post on this setup&lt;/a&gt; — there the AI added invented keys &lt;em&gt;inside&lt;/em&gt; a structurally valid file; here it wrote a syntactically invalid line &lt;em&gt;outside&lt;/em&gt; the file entirely. Same class of problem, different location. Lesson either way: after any AI-assisted config edit, run &lt;code&gt;openclaw doctor&lt;/code&gt; before restarting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The schema migration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;openclaw doctor&lt;/code&gt; run during a config hardening session flagged a format change:&lt;/p&gt;

&lt;p&gt;Before: &lt;code&gt;streaming: "partial"&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;After:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;streaming:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;mode:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"partial"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fix: &lt;code&gt;openclaw doctor --fix&lt;/code&gt; migrated it in-place. Restart.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;openclaw doctor --fix&lt;/code&gt; is a migration tool, not just a health check. If your config has drifted across versions, it catches and repairs format changes in one command. Run it after upgrades.&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Becomes
&lt;/h2&gt;

&lt;p&gt;After the config keys. After the two-system approval fix. After the schema migration. Telegram works — fully works.&lt;/p&gt;

&lt;p&gt;And then Whisper.&lt;/p&gt;

&lt;p&gt;OpenClaw routes incoming Telegram voice messages through a local Whisper server and passes the transcription to the agent as text, as if you had typed it. Three things make this work once Telegram is stable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;faster-whisper-server&lt;/code&gt; running locally: CPU inference, &lt;code&gt;base&lt;/code&gt; model, port 8765&lt;/li&gt;
&lt;li&gt;Two env vars in the gateway's systemd environment: &lt;code&gt;OPENAI_BASE_URL=http://127.0.0.1:8765/v1&lt;/code&gt; and &lt;code&gt;OPENAI_API_KEY=local&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;One non-obvious requirement: the &lt;code&gt;openai-whisper-api&lt;/code&gt; skill defaults to &lt;code&gt;response_format=text&lt;/code&gt; in its &lt;code&gt;transcribe.sh&lt;/code&gt;. Local Whisper servers require &lt;code&gt;response_format=json&lt;/code&gt; — the &lt;code&gt;text&lt;/code&gt; format fails silently. The gateway drops the voice message with no error, no log entry, no indication anything arrived. If voice messages are sending but nothing is responding, that value is the first thing to check. Editing it in the skill file is reset on upgrade, which is why the full installation walkthrough is in a dedicated video and follow-up post in this series.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With that in place: you record a voice note on your phone. Send it. The local Whisper instance transcribes it on your server — no cloud service involved. The agent responds to what you said out loud.&lt;/p&gt;

&lt;p&gt;You are not at a keyboard. You are walking, in another room, hands full. You ask a question and your agent answers it.&lt;/p&gt;

&lt;p&gt;That is what changes when Telegram is fully and correctly configured. The interface disappears. The phone becomes a terminal that listens.&lt;/p&gt;

&lt;p&gt;Everything in this post — the config keys, the two approval systems, the schema migration — is the cost of admission to that experience. It is worth paying.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd Do Differently From the Start
&lt;/h2&gt;

&lt;p&gt;If I were setting this up again today, here is what I would do in the first ten minutes instead of discovering it across multiple sessions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Set &lt;code&gt;groupAllowFrom&lt;/code&gt; (or &lt;code&gt;allowFrom&lt;/code&gt;) alongside &lt;code&gt;groupPolicy: "allowlist"&lt;/code&gt; — not the policy alone.&lt;/strong&gt; Get your numeric Telegram user ID first via the gateway logs or the Bot API (both methods are in &lt;em&gt;The Keys, and What Happens Without Them&lt;/em&gt; above), then put the number in &lt;code&gt;groupAllowFrom&lt;/code&gt;. The policy without a populated allowlist — and with &lt;code&gt;allowFrom&lt;/code&gt; also empty — silently locks everyone out of groups, including you.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Disable both exec approval systems, not just one.&lt;/strong&gt; System 1: &lt;code&gt;channels.telegram.execApprovals.enabled: false&lt;/code&gt; in &lt;code&gt;openclaw.json&lt;/code&gt;. System 2: &lt;code&gt;~/.openclaw/exec-approvals.json&lt;/code&gt; with &lt;code&gt;defaults.security: "full"&lt;/code&gt;, &lt;code&gt;ask: "off"&lt;/code&gt;, and &lt;code&gt;askFallback: "full"&lt;/code&gt; — plus the gateway-side policy (&lt;code&gt;openclaw exec-policy preset yolo&lt;/code&gt; sets both at once). Disabling only the first leaves the second still prompting. The session that revealed this was not short.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Run &lt;code&gt;openclaw doctor&lt;/code&gt; after every AI-assisted config edit.&lt;/strong&gt; The AI will write outside the JSON5 structure at some point. &lt;code&gt;openclaw doctor&lt;/code&gt; catches config problems before they become a gateway that refuses to start with no useful explanation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Run &lt;code&gt;openclaw doctor&lt;/code&gt; after every upgrade.&lt;/strong&gt; It is a migration tool. If your config has drifted across versions, &lt;code&gt;--fix&lt;/code&gt; catches and repairs it in one command.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One open item: live Telegram-path verification on the &lt;code&gt;2026.4.21&lt;/code&gt; runtime is still pending. The integration was confirmed working on the version before that upgrade. I will update this post when that test is complete.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;groupAllowFrom&lt;/code&gt; was your silent failure — or if you hit the two-system exec approval problem and only found one of the fixes — drop a comment. I want to know what the guides are missing.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: AI, Self-Hosting, DevOps, Telegram, Software Engineering&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>openclaw</category>
      <category>learning</category>
    </item>
    <item>
      <title>From Broken Docker Containers to a Working AI Agent: The Full OpenClaw Journey</title>
      <dc:creator>sir'Alexander</dc:creator>
      <pubDate>Sun, 05 Apr 2026 15:53:27 +0000</pubDate>
      <link>https://dev.to/sir_alexander_t/from-broken-docker-containers-to-a-working-ai-agent-the-full-openclaw-journey-3mc0</link>
      <guid>https://dev.to/sir_alexander_t/from-broken-docker-containers-to-a-working-ai-agent-the-full-openclaw-journey-3mc0</guid>
      <description>&lt;p&gt;&lt;strong&gt;Every blocker, every fix, and why bare metal is the sweet spot for a personal AI agent setup.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I spent several weeks self-hosting OpenClaw on a Hetzner VPS. The setup involved more blockers than I expected — a 3-day crash loop caused by a model hallucinating its own config, a Docker isolation wall that prevented the agent from using any tools I didn't bake into the image at build time, and a browser control problem I had completely misunderstood. I migrated to bare metal. Everything works now. Here's the full story with every exact fix.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is OpenClaw and why did I want it?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://openclaw.io" rel="noopener noreferrer"&gt;OpenClaw&lt;/a&gt; is an open-source AI agent gateway. You connect it to Telegram (or a web chat), point it at a model, and you get a personal AI agent that can browse the web, read files, run code, and respond to messages. The selling point is control: you pick the model, you own the server, you pay for it if you want or run it for free with community models.&lt;/p&gt;

&lt;p&gt;I wanted a personal AI agent that could take real actions — not just answer questions. One I could message from my phone and have it actually do things. A Hetzner 4GB VPS at around €5/month seemed like the right place to start.&lt;/p&gt;

&lt;p&gt;Here is what actually happened.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1: Getting Docker running (it took longer than it should have)
&lt;/h2&gt;

&lt;p&gt;The official setup uses Docker. You clone the repo, configure a &lt;code&gt;.env&lt;/code&gt; file, and run a setup script. Straightforward in theory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blocker 1: &lt;code&gt;Cannot find package 'nostr-tools'&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first time I ran the gateway, it crashed immediately with this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Cannot find package 'nostr-tools'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After digging into the repo, the root cause was simple: the Docker image needs to be built with the &lt;code&gt;nostr&lt;/code&gt; extension included, and if you don't pass it explicitly, the build skips it.&lt;/p&gt;

&lt;p&gt;Fix: add this to &lt;code&gt;docker-compose.yml&lt;/code&gt; under the build section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;OPENCLAW_EXTENSIONS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nostr&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then rebuild the image with &lt;code&gt;docker compose build&lt;/code&gt; before running. That cleared it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blocker 2: The &lt;code&gt;.env&lt;/code&gt; trap&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The setup script uses an environment variable called &lt;code&gt;OPENCLAW_IMAGE&lt;/code&gt; to know which Docker image to use. I had it set in my &lt;code&gt;.env&lt;/code&gt; file. The script silently ignored it.&lt;/p&gt;

&lt;p&gt;The fix is to export it in the shell before running the script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENCLAW_IMAGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;openclaw:local
./scripts/docker/setup.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setting it in &lt;code&gt;.env&lt;/code&gt; is not enough — the script doesn't source the file. This one cost me more time than it should have because there was no error, just the wrong image being used.&lt;/p&gt;

&lt;p&gt;After both fixes: the gateway started, and &lt;code&gt;/healthz&lt;/code&gt; returned &lt;code&gt;{"ok":true}&lt;/code&gt;. First win.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2: Browser control — I had the wrong mental model
&lt;/h2&gt;

&lt;p&gt;I wanted the agent to be able to browse the web. OpenClaw supports browser control via Chrome DevTools Protocol. I installed Google Chrome on the VPS host, enabled browser control in the config, and expected it to work.&lt;/p&gt;

&lt;p&gt;It didn't. The agent couldn't find the browser at all.&lt;/p&gt;

&lt;p&gt;The problem was my mental model. The gateway runs inside a Docker container. The browser I installed is on the host. The container has no visibility into the host's binaries. The browser has to be inside the container.&lt;/p&gt;

&lt;p&gt;The fix is to rebuild the image with Chromium and the required display server baked in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENCLAW_DOCKER_APT_PACKAGES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"chromium xvfb xauth"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENCLAW_IMAGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;openclaw:local
./scripts/docker/setup.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the rebuild, &lt;code&gt;openclaw browser open https://example.com&lt;/code&gt; worked. &lt;code&gt;openclaw browser snapshot&lt;/code&gt; printed the page structure. Browser control was live.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3: The 3-day crash loop (this one was subtle)
&lt;/h2&gt;

&lt;p&gt;About three weeks in, the gateway started crash-looping. It would start, run for a few seconds, and crash. Every time.&lt;/p&gt;

&lt;p&gt;I spent a while looking at the logs before I found the problem. Someone had appended a stray line after the closing &lt;code&gt;}&lt;/code&gt; in &lt;code&gt;openclaw.json&lt;/code&gt;, making the whole file invalid JSON5:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hocon"&gt;&lt;code&gt;&lt;span class="c1"&gt;// end of the file, after the closing brace:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;channels.telegram.groupAllowFrom&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;your-telegram-id&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stripping that line fixed the first crash. But the gateway crashed again.&lt;/p&gt;

&lt;p&gt;This time, the config itself was valid JSON, but it contained these keys in the &lt;code&gt;session&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"session"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dmScope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"per-channel-peer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"persistence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"autoSaveInterval"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300000&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;persistence&lt;/code&gt; and &lt;code&gt;autoSaveInterval&lt;/code&gt; do not exist in the OpenClaw config schema. The gateway was rejecting them on startup.&lt;/p&gt;

&lt;p&gt;I had not written those keys. The AI agent had. During a previous session, the agent had edited its own config file and invented plausible-sounding but completely invalid configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this happened: the model quality problem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The default model in a fresh OpenClaw setup routes to &lt;code&gt;openrouter/free&lt;/code&gt;. That endpoint cycles through whatever free models are available — often small ones with 4,000–8,000 token context windows. In a long enough session, the model loses context and starts guessing. It guessed config keys that sounded reasonable but didn't exist.&lt;/p&gt;

&lt;p&gt;The fix was to upgrade the model before anything else.&lt;/p&gt;

&lt;p&gt;I switched to &lt;a href="https://kimi.moonshot.cn/" rel="noopener noreferrer"&gt;Kimi K2.5&lt;/a&gt; via Ollama's cloud model feature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"agents"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"defaults"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"primary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ollama/kimi-k2.5:cloud"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"fallbacks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ollama/minimax-m2.5:cloud"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ollama/glm-5:cloud"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"openrouter/free"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kimi K2.5 is free, runs on Moonshot's infrastructure (no VPS RAM impact), and has a 131k token context window. The community consistently ranks it as the top free model for agentic tasks. The fabricated config problem disappeared.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson here is uncomfortable:&lt;/strong&gt; if your agent edits its own config and the model is bad enough, it will silently break itself. Model quality is not optional once you're running long agentic sessions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 4: The moment Docker stopped being enough
&lt;/h2&gt;

&lt;p&gt;After sorting the model, everything felt stable. Telegram was working. Browser control was working. Then I asked the agent to read a PDF.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pdftotext: command not found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I could install &lt;code&gt;pdftotext&lt;/code&gt; on the host easily enough. But the agent can't see the host. It can't run &lt;code&gt;apt-get install&lt;/code&gt; inside a running container. It can't reach the host filesystem outside the mounted volume.&lt;/p&gt;

&lt;p&gt;This isn't a config problem. It's the design of Docker. Containers are isolated. That isolation is useful for security and repeatability, but it means the agent's tool access is fixed at image build time. Every new capability requires a full image rebuild.&lt;/p&gt;

&lt;p&gt;For a personal AI agent — where the whole point is that it can do anything you'd do at a terminal — that's the wrong tradeoff.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 5: The bare metal migration
&lt;/h2&gt;

&lt;p&gt;Once I understood the Docker wall as a structural constraint rather than a config problem, the migration decision was straightforward. OpenClaw supports &lt;code&gt;npm install -g openclaw&lt;/code&gt; plus a systemd service on bare metal. The agent gets full host access.&lt;/p&gt;

&lt;p&gt;Here's what the migration actually involved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Node.js 24&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ubuntu's default Node.js is outdated. Install via NodeSource:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://deb.nodesource.com/setup_24.x | bash -
apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nodejs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Google Chrome &lt;code&gt;.deb&lt;/code&gt; — not the snap&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The snap version of Chromium breaks under systemd. AppArmor blocks Chrome DevTools Protocol from binding. You need the &lt;code&gt;.deb&lt;/code&gt; package from Google directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
apt &lt;span class="nb"&gt;install&lt;/span&gt; ./google-chrome-stable_current_amd64.deb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify headless mode works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;google-chrome &lt;span class="nt"&gt;--headless&lt;/span&gt; &lt;span class="nt"&gt;--no-sandbox&lt;/span&gt; &lt;span class="nt"&gt;--dump-dom&lt;/span&gt; https://example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that returns HTML, you're good.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Install OpenClaw and update the config&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; openclaw
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then update two values in &lt;code&gt;openclaw.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Change&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Ollama&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;URL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Docker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;bridge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;localhost&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"models"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"providers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ollama"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"baseUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:11434"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Update&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;workspace&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Docker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;container&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;path&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"agents"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"defaults"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"workspace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/home/&amp;lt;your-user&amp;gt;/.openclaw/workspace"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4: Stop Docker, start the systemd service&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose down
openclaw gateway &lt;span class="nb"&gt;install
&lt;/span&gt;openclaw gateway start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check both health endpoints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:3000/healthz   &lt;span class="c"&gt;# {"ok":true}&lt;/span&gt;
curl http://localhost:3000/readyz    &lt;span class="c"&gt;# {"ready":true}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Chrome lock file problem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First browser open attempt failed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Failed to start Chrome CDP — profile appears to be in use by another process
(hostname: &amp;lt;docker-container-id&amp;gt;)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That hostname is the old Docker container. It had left lock files behind in the Chrome profile directory. Remove them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; ~/.openclaw/browser/openclaw/user-data/Singleton&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, &lt;code&gt;openclaw browser open https://example.com&lt;/code&gt; succeeded. Browser control working on bare metal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Install the tools the agent actually needs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now that the agent has host access, this is just a one-liner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;poppler-utils  &lt;span class="c"&gt;# pdftotext&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No image rebuild. No container restart. The agent can now read PDFs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 6: The verdict
&lt;/h2&gt;

&lt;p&gt;After all of this, here's where I landed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Docker&lt;/th&gt;
&lt;th&gt;Bare Metal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Agent tool access&lt;/td&gt;
&lt;td&gt;Sealed — no installs at runtime&lt;/td&gt;
&lt;td&gt;Full host access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser control&lt;/td&gt;
&lt;td&gt;Chromium + Xvfb inside container&lt;/td&gt;
&lt;td&gt;Headless Chrome &lt;code&gt;.deb&lt;/code&gt; on host&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model quality&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;openrouter/free&lt;/code&gt; default — dangerous&lt;/td&gt;
&lt;td&gt;Kimi K2.5 cloud — 131k context, free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RAM overhead&lt;/td&gt;
&lt;td&gt;Higher&lt;/td&gt;
&lt;td&gt;~1–1.5 GB idle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PDF / host tools&lt;/td&gt;
&lt;td&gt;Blocked&lt;/td&gt;
&lt;td&gt;Works natively&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Update process&lt;/td&gt;
&lt;td&gt;Rebuild image&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm install -g openclaw&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Isolated multi-user setups&lt;/td&gt;
&lt;td&gt;Single-user VPS — the sweet spot&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Docker is not wrong. For multi-user setups or anything security-critical, the isolation it provides is valuable. But for a personal AI agent on a VPS you control, it's the wrong default. The agent's whole value is its ability to act. Sealing it in a container limits what it can act on.&lt;/p&gt;

&lt;p&gt;Bare metal gives you a lighter setup, simpler operations, and a proper agent.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I would do differently from the start
&lt;/h2&gt;

&lt;p&gt;If I were starting over today, these are the things I'd do in the first ten minutes rather than discovering them the hard way:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Upgrade the model immediately.&lt;/strong&gt; Before anything else. &lt;code&gt;openrouter/free&lt;/code&gt; is dangerous for agents that modify their own state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use bare metal from day one&lt;/strong&gt; if you're on a personal VPS and you're the only user. Docker adds overhead and constraints that don't pay off in this scenario.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export environment variables in your shell&lt;/strong&gt;, not just &lt;code&gt;.env&lt;/code&gt;, when running setup scripts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Google Chrome &lt;code&gt;.deb&lt;/code&gt;, not the snap.&lt;/strong&gt; AppArmor will kill you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the config minimal.&lt;/strong&gt; If you're not sure a config key exists, don't add it. Let the agent work with valid defaults before customising.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Current state
&lt;/h2&gt;

&lt;p&gt;The gateway has been running on bare metal for several weeks. Telegram is working. Browser control is working. The agent can read PDFs, run commands, and manage files. Kimi K2.5 hasn't hallucinated a single config key.&lt;/p&gt;

&lt;p&gt;If you're running OpenClaw or thinking about it, drop a comment. I'm curious what setups others are running and whether the Docker-to-bare-metal migration pattern holds up across different VPS providers.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: AI, Self-Hosting, DevOps, Docker, Software Engineering&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>docker</category>
      <category>openclaw</category>
    </item>
  </channel>
</rss>
