<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Signal on Alfero Chingono</title><link>https://www.chingono.com/tags/signal/</link><description>Recent content in Signal on Alfero Chingono</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Sun, 05 Apr 2026 11:49:27 -0400</lastBuildDate><atom:link href="https://www.chingono.com/tags/signal/index.xml" rel="self" type="application/rss+xml"/><item><title>Why My OpenClaw Reminders Weren't Reaching Signal or Teams</title><link>https://www.chingono.com/blog/2026/04/04/why-my-openclaw-reminders-werent-reaching-signal-or-teams/</link><pubDate>Sat, 04 Apr 2026 21:23:46 +0000</pubDate><guid>https://www.chingono.com/blog/2026/04/04/why-my-openclaw-reminders-werent-reaching-signal-or-teams/</guid><description>&lt;img src="https://www.chingono.com/blog/2026/04/04/why-my-openclaw-reminders-werent-reaching-signal-or-teams/cover.png" alt="Featured image of post Why My OpenClaw Reminders Weren't Reaching Signal or Teams" /&gt;&lt;p&gt;This is the fifth and final post in my short OpenClaw series. If you want the background first, read &lt;a class="link" href="https://www.chingono.com/blog/2026/03/05/why-i-run-openclaw-in-docker-on-my-own-machine/" &gt;why I run OpenClaw in Docker&lt;/a&gt;, &lt;a class="link" href="https://www.chingono.com/blog/2026/03/15/how-i-wired-signal-and-microsoft-teams-into-a-custom-openclaw-image/" &gt;how I wired Signal and Teams into a custom image&lt;/a&gt;, &lt;a class="link" href="https://www.chingono.com/blog/2026/03/15/inside-the-dockerfile-behind-my-openclaw-gateway/" &gt;the Dockerfile walkthrough&lt;/a&gt;, and &lt;a class="link" href="https://www.chingono.com/blog/2026/04/03/how-i-split-openclaw-into-main-and-personal-agents/" &gt;the main/personal agent split&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I ran into an OpenClaw bug this week that annoyed me more than a normal delivery failure would have.&lt;/p&gt;
&lt;p&gt;A reminder was supposed to come back to me later telling me to submit a GO Transit delay claim. The agent said it had scheduled the reminder. Then later&amp;hellip; nothing showed up in Signal. Nothing showed up in Teams either.&lt;/p&gt;
&lt;p&gt;That kind of bug is worse than a simple send failure because it creates false confidence. If a reminder system quietly drops the reminder, that&amp;rsquo;s bad. If it tells you the reminder is set when it actually isn&amp;rsquo;t, that&amp;rsquo;s much worse.&lt;/p&gt;
&lt;p&gt;The fix turned out to be pretty specific: reminder jobs needed to preserve the &lt;strong&gt;originating session route&lt;/strong&gt;, not just &amp;ldquo;some channel&amp;rdquo; to send to later.&lt;/p&gt;
&lt;h2 id="the-symptom-wasnt-just-signal-is-broken"&gt;The symptom wasn&amp;rsquo;t just &amp;ldquo;Signal is broken&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;The first thing I did was inspect the stored cron jobs and their run logs.&lt;/p&gt;
&lt;p&gt;I found two failed reminder jobs in OpenClaw&amp;rsquo;s cron state. Both had ended with the same kind of error:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Error: Signal RPC -1: Failed to send message
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;At first glance, that looks like a Signal transport problem. But that theory fell apart pretty quickly.&lt;/p&gt;
&lt;p&gt;I sent a proactive Signal message directly through OpenClaw&amp;rsquo;s normal outbound path, outside cron, and it worked:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;openclaw session send --agent personal --message &lt;span class="s2"&gt;&amp;#34;test delivery&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;That mattered a lot.&lt;/p&gt;
&lt;p&gt;It meant Signal itself was healthy. The account was connected. The RPC path was fine. Proactive outbound delivery was possible. So the bug wasn&amp;rsquo;t &amp;ldquo;OpenClaw can&amp;rsquo;t send Signal messages.&amp;rdquo; The bug was narrower: &lt;strong&gt;cron-delivered reminders were failing in their announce/delivery path&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id="the-failure-was-hiding-behind-a-second-bug"&gt;The failure was hiding behind a second bug
&lt;/h2&gt;&lt;p&gt;While tracing the original conversation, I found something even more frustrating.&lt;/p&gt;
&lt;p&gt;The first time the agent tried to create the reminder, it used invalid CLI flags. Then it tried again with another invalid form. Then it hit a gateway error. After that, instead of telling the user scheduling had failed, it wrote notes into &lt;code&gt;HEARTBEAT.md&lt;/code&gt; and &lt;code&gt;MEMORY.md&lt;/code&gt; and still acted as if the reminder had been set.&lt;/p&gt;
&lt;p&gt;Not a reminder — that&amp;rsquo;s a private note pretending to be one.&lt;/p&gt;
&lt;p&gt;Later in the same session, the agent finally did create a real cron job. So there were actually two different problems:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;the agent could falsely claim success after &lt;code&gt;cron add&lt;/code&gt; failed&lt;/li&gt;
&lt;li&gt;even when a real reminder job existed, delivery could still fail later&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I wanted both fixed.&lt;/p&gt;
&lt;h2 id="the-real-clue-was-in-the-job-metadata"&gt;The real clue was in the job metadata
&lt;/h2&gt;&lt;p&gt;The failed Signal reminder job looked roughly like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;delivery&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;mode&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;announce&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;channel&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;signal&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;to&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;uuid:...&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;agentId&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;sessionKey&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;That looked suspicious immediately.&lt;/p&gt;
&lt;p&gt;OpenClaw reminder jobs run in an isolated cron session. That isolation is useful, but it also means the job needs enough routing context to find its way back to the human who asked for it.&lt;/p&gt;
&lt;p&gt;The important detail here is that &amp;ldquo;send a reminder later&amp;rdquo; is not just content generation. It&amp;rsquo;s also a routing problem.&lt;/p&gt;
&lt;p&gt;The reminder has to know:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;which agent owns the conversation&lt;/li&gt;
&lt;li&gt;which exact session started the request&lt;/li&gt;
&lt;li&gt;which channel to send back to&lt;/li&gt;
&lt;li&gt;which exact recipient or thread target to use&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Without that information, the agent can successfully generate the reminder summary and still fail at the final delivery step.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s exactly what was happening.&lt;/p&gt;
&lt;p&gt;The cron run succeeded at the AI part. It produced a perfectly good reminder summary. Then the last hop failed.&lt;/p&gt;
&lt;h2 id="same-channel-delivery-was-the-right-rule"&gt;Same-channel delivery was the right rule
&lt;/h2&gt;&lt;p&gt;There was one behavioral clarification that mattered a lot here: reminders should go back to the &lt;strong&gt;channel that initiated the request&lt;/strong&gt;, not to every connected channel.&lt;/p&gt;
&lt;p&gt;That sounds obvious once you say it out loud, but it changes the design.&lt;/p&gt;
&lt;p&gt;If I asked for a reminder in Signal, I want it back in that Signal conversation.&lt;/p&gt;
&lt;p&gt;If I asked for it in a Teams chat or thread, I want it back there.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t want a reminder system that gets &amp;ldquo;helpful&amp;rdquo; and starts spraying notifications across every channel it knows about. That&amp;rsquo;s not smarter. That&amp;rsquo;s just noisier.&lt;/p&gt;
&lt;p&gt;This also means the exact route matters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Signal direct chats may use UUID-form targets&lt;/li&gt;
&lt;li&gt;Teams direct chats and Teams channel threads have different route shapes&lt;/li&gt;
&lt;li&gt;a thread target is not interchangeable with a generic user target&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So &amp;ldquo;channel = signal&amp;rdquo; or &amp;ldquo;channel = msteams&amp;rdquo; is not enough by itself. The job has to preserve the full conversation identity.&lt;/p&gt;
&lt;h2 id="healthy-jobs-were-already-telling-me-what-the-fix-should-be"&gt;Healthy jobs were already telling me what the fix should be
&lt;/h2&gt;&lt;p&gt;One of the most useful clues came from looking at working reminder jobs that were already stored in the system.&lt;/p&gt;
&lt;p&gt;Existing Teams reminder jobs already had both of these fields populated:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;agentId&amp;#34;&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;main&amp;#34;&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;sessionKey&amp;#34;&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;agent:main:msteams:channel:...&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;That was the pattern the broken Signal jobs were missing.&lt;/p&gt;
&lt;p&gt;Once I saw that, the problem became much clearer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;healthy jobs preserved the originating session route&lt;/li&gt;
&lt;li&gt;broken jobs only had partial delivery info&lt;/li&gt;
&lt;li&gt;cron isolation meant partial delivery info was not reliable enough&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the fix wasn&amp;rsquo;t &amp;ldquo;retry Signal harder.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;The fix was &amp;ldquo;preserve the route that the reminder belongs to.&amp;rdquo;&lt;/p&gt;
&lt;h2 id="what-i-changed"&gt;What I changed
&lt;/h2&gt;&lt;p&gt;I made two concrete changes.&lt;/p&gt;
&lt;h3 id="1-i-updated-the-agent-workspace-instructions"&gt;1. I updated the agent workspace instructions
&lt;/h3&gt;&lt;p&gt;I added explicit reminder-delivery rules to both the &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;personal&lt;/code&gt; agent workspaces so future reminder jobs must:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;send only to the chat that asked for the reminder&lt;/li&gt;
&lt;li&gt;include &lt;code&gt;--agent &amp;lt;current-agent-id&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;include &lt;code&gt;--session-key &amp;lt;current-session-key&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;reuse the exact &lt;code&gt;--channel&lt;/code&gt; and &lt;code&gt;--to&lt;/code&gt; from the current session&lt;/li&gt;
&lt;li&gt;tell the user plainly if &lt;code&gt;cron add&lt;/code&gt; fails&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The practical shape now looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;openclaw cron add &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --name &lt;span class="s2"&gt;&amp;#34;...&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --at &lt;span class="s2"&gt;&amp;#34;...&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --announce &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --agent main &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --session-key agent:main:signal:direct:uuid:&amp;lt;recipient&amp;gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --channel signal &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --to uuid:&amp;lt;recipient&amp;gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --message &lt;span class="s2"&gt;&amp;#34;...&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;If the reminder started in Teams, the same rule applies with the Teams session key and exact Teams target.&lt;/p&gt;
&lt;h3 id="2-i-repaired-the-already-failed-jobs"&gt;2. I repaired the already-failed jobs
&lt;/h3&gt;&lt;p&gt;I patched the stored failed Signal reminder jobs so they now carry the missing &lt;code&gt;agentId&lt;/code&gt;, &lt;code&gt;sessionKey&lt;/code&gt;, and explicit destination metadata.&lt;/p&gt;
&lt;p&gt;First I listed the jobs to identify the broken ones:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;openclaw cron list
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Then I edited each failed job to restore the routing context:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;openclaw cron edit &amp;lt;job-id&amp;gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --announce &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --agent personal &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --session-key agent:personal:signal:direct:uuid:&amp;lt;recipient&amp;gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --channel signal &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --to uuid:&amp;lt;recipient&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;That gave me a clean way to test the actual failing path instead of just assuming the theory was right.&lt;/p&gt;
&lt;h2 id="the-result"&gt;The result
&lt;/h2&gt;&lt;p&gt;After repairing the routing metadata, I reran the missed transit-claim reminder job:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;openclaw cron run &amp;lt;job-id&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Previously its run log ended like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;status: error
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;deliveryStatus: unknown
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Error: Signal RPC -1: Failed to send message
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;After the fix, the rerun finished like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;status: ok
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;deliveryStatus: delivered
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;delivered: true
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;That one successful rerun told me a lot.&lt;/p&gt;
&lt;p&gt;It confirmed that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the reminder content generation path was fine&lt;/li&gt;
&lt;li&gt;Signal itself was fine&lt;/li&gt;
&lt;li&gt;the broken piece was the missing session routing metadata&lt;/li&gt;
&lt;li&gt;preserving the original route fixed the actual production failure&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Because it was a one-shot reminder, the job then removed itself after succeeding, which is exactly what I wanted.&lt;/p&gt;
&lt;h2 id="the-bigger-lesson"&gt;The bigger lesson
&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;ve been spending a lot of time thinking about multi-agent systems, orchestration, and tool contracts lately, and this bug fit that theme perfectly.&lt;/p&gt;
&lt;p&gt;In agent systems, routing context is not incidental metadata. It&amp;rsquo;s part of the work.&lt;/p&gt;
&lt;p&gt;If a background task is supposed to come back to a human later, then &amp;ldquo;who asked for this?&amp;rdquo; and &amp;ldquo;where should it return?&amp;rdquo; are first-class data, not optional fields you can fill in later if you feel like it.&lt;/p&gt;
&lt;p&gt;The other lesson is even simpler: &lt;strong&gt;don&amp;rsquo;t claim automation succeeded unless it actually did&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;That sounds almost embarrassingly obvious, but it&amp;rsquo;s the kind of failure mode that destroys trust very quickly. A failed reminder should be reported as failed. A note in memory isn&amp;rsquo;t a substitute for delivery. And a scheduler shouldn&amp;rsquo;t quietly lose the route back to the person who asked for the work.&lt;/p&gt;
&lt;p&gt;Small bug. Important fix.&lt;/p&gt;
&lt;p&gt;And honestly, those are often the bugs worth writing about.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re interested in the broader design side of this kind of system, I wrote more about &lt;a class="link" href="https://www.chingono.com/blog/2025/02/15/why-i-started-building-my-own-devops-platform-and-what-i-learned/" &gt;why I started building my own DevOps platform&lt;/a&gt;, &lt;a class="link" href="https://www.chingono.com/blog/2025/03/20/mcp-in-practice-what-anthropics-model-context-protocol-actually-means-for-developers/" &gt;what MCP changed for me in practice&lt;/a&gt;, and &lt;a class="link" href="https://www.chingono.com/blog/2025/08/28/designing-multi-agent-systems-lessons-from-building-an-8-agent-engineering-orchestra/" &gt;what building a real multi-agent system taught me about roles, boundaries, and orchestration&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>How I Split OpenClaw into Main and Personal Agents</title><link>https://www.chingono.com/blog/2026/04/03/how-i-split-openclaw-into-main-and-personal-agents/</link><pubDate>Fri, 03 Apr 2026 09:00:00 +0000</pubDate><guid>https://www.chingono.com/blog/2026/04/03/how-i-split-openclaw-into-main-and-personal-agents/</guid><description>&lt;img src="https://www.chingono.com/blog/2026/04/03/how-i-split-openclaw-into-main-and-personal-agents/cover.png" alt="Featured image of post How I Split OpenClaw into Main and Personal Agents" /&gt;&lt;p&gt;By this point in the series, the Docker stack was solid, the custom image was working, and both channel integrations were live. The next question wasn&amp;rsquo;t purely technical — it was architectural.&lt;/p&gt;
&lt;p&gt;Should one agent answer everything?&lt;/p&gt;
&lt;p&gt;I didn&amp;rsquo;t think so.&lt;/p&gt;
&lt;h2 id="i-wanted-separation-without-turning-the-system-into-theater"&gt;I wanted separation without turning the system into theater
&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;m generally skeptical of fake multi-agent setups where the same assistant just changes costumes.&lt;/p&gt;
&lt;p&gt;What I wanted here was simpler and more practical:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;one agent for the main, more operational surface&lt;/li&gt;
&lt;li&gt;another agent for the more personal, direct-chat surface&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&amp;rsquo;s how I ended up with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Marshal&lt;/strong&gt; as the &lt;code&gt;main&lt;/code&gt; agent&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ava&lt;/strong&gt; as the &lt;code&gt;personal&lt;/code&gt; agent&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This was not about branding. It was about boundaries.&lt;/p&gt;
&lt;h2 id="the-split-shows-up-directly-in-config"&gt;The split shows up directly in config
&lt;/h2&gt;&lt;p&gt;The OpenClaw config defines both the shared defaults and the named agent list:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;agents&amp;#34;&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;defaults&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;workspace&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/home/node/.openclaw/workspaces/main&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;list&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;main&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Marshal&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;workspace&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/home/node/.openclaw/workspaces/main&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;personal&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Ava&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;workspace&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/home/node/.openclaw/workspaces/personal&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;What I like is that it makes the split durable.&lt;/p&gt;
&lt;p&gt;This isn&amp;rsquo;t &amp;ldquo;try to remember which tone to use.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;It is separate agent identity, separate workspace, separate routing rules, and separate session histories.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s a much stronger foundation.&lt;/p&gt;
&lt;h2 id="separate-workspaces-were-part-of-the-point"&gt;Separate workspaces were part of the point
&lt;/h2&gt;&lt;p&gt;Each agent gets its own workspace directory.&lt;/p&gt;
&lt;p&gt;That matters because workspaces are not just folders. They&amp;rsquo;re where the agent&amp;rsquo;s local memory, notes, conventions, and operating context live.&lt;/p&gt;
&lt;p&gt;In my setup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;main&lt;/code&gt; uses &lt;code&gt;/home/node/.openclaw/workspaces/main&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;personal&lt;/code&gt; uses &lt;code&gt;/home/node/.openclaw/workspaces/personal&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both workspaces can still see the mounted project repositories, but they do not have to behave like the same conversational surface.&lt;/p&gt;
&lt;p&gt;I like that a lot.&lt;/p&gt;
&lt;p&gt;It lets the system stay coherent without forcing every interaction through the same shared context.&lt;/p&gt;
&lt;h2 id="the-channel-bindings-made-the-split-real"&gt;The channel bindings made the split real
&lt;/h2&gt;&lt;p&gt;The cleanest part of the setup is the routing section:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;bindings&amp;#34;&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;route&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;agentId&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;personal&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;match&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;channel&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;signal&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;route&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;agentId&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;main&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;match&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;channel&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;msteams&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;That means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Signal routes to &lt;code&gt;personal&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Teams routes to &lt;code&gt;main&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I like this because it reflects how I actually use those channels.&lt;/p&gt;
&lt;p&gt;Signal is more personal and direct. Teams is more operational, thread-oriented, work-shaped. The routing isn&amp;rsquo;t arbitrary — it maps the social surface to the agent surface.&lt;/p&gt;
&lt;h2 id="the-rest-of-the-config-reinforces-the-separation"&gt;The rest of the config reinforces the separation
&lt;/h2&gt;&lt;p&gt;A few other config details matter more than they might look.&lt;/p&gt;
&lt;p&gt;One is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;session&amp;#34;&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;dmScope&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;per-channel-peer&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;That keeps DM scope tied to the peer within each channel, which makes the session model predictable. A Signal DM and a Teams DM aren&amp;rsquo;t secretly the same conversation just because they involve the same human.&lt;/p&gt;
&lt;p&gt;Another is the Teams reply behavior:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;replyStyle&amp;#34;&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;thread&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;It means routing isn&amp;rsquo;t just &amp;ldquo;channel = Teams.&amp;rdquo; The conversation can carry thread structure too, which becomes very relevant once background tasks and reminders need to find their way back to the right place.&lt;/p&gt;
&lt;h2 id="the-model-defaults-stay-shared-but-the-identity-does-not"&gt;The model defaults stay shared, but the identity does not
&lt;/h2&gt;&lt;p&gt;Both agents inherit the same broad model strategy from &lt;code&gt;agents.defaults&lt;/code&gt;: cloud-first, with fallbacks, and Ollama available as the local last resort.&lt;/p&gt;
&lt;p&gt;I think that&amp;rsquo;s the right trade.&lt;/p&gt;
&lt;p&gt;I didn&amp;rsquo;t need two completely different intelligence stacks. I needed two differently situated agents.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s a useful distinction.&lt;/p&gt;
&lt;p&gt;Too many AI systems try to create variety through persona language alone. I trust structural differences more than stylistic ones.&lt;/p&gt;
&lt;h2 id="this-setup-made-later-routing-bugs-easier-to-diagnose"&gt;This setup made later routing bugs easier to diagnose
&lt;/h2&gt;&lt;p&gt;One side effect of the split is that it made the reminder-delivery bug much easier to reason about later.&lt;/p&gt;
&lt;p&gt;Because the system was already explicit about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;which agent owned which channel&lt;/li&gt;
&lt;li&gt;which workspace belonged to which agent&lt;/li&gt;
&lt;li&gt;which session route belonged to which conversation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;it became much easier to spot when a background reminder job had lost the routing context it needed.&lt;/p&gt;
&lt;p&gt;In other words, the multi-agent setup wasn&amp;rsquo;t the source of the bug.&lt;/p&gt;
&lt;p&gt;It was part of what made the bug diagnosable.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s a nice example of why explicit structure pays off.&lt;/p&gt;
&lt;h2 id="my-takeaway"&gt;My takeaway
&lt;/h2&gt;&lt;p&gt;I don&amp;rsquo;t think every AI setup needs multiple agents.&lt;/p&gt;
&lt;p&gt;But if you&amp;rsquo;re going to split them, I think the split should show up in real places:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;workspaces&lt;/li&gt;
&lt;li&gt;routing rules&lt;/li&gt;
&lt;li&gt;session boundaries&lt;/li&gt;
&lt;li&gt;permissions&lt;/li&gt;
&lt;li&gt;operational expectations&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Otherwise you&amp;rsquo;re mostly decorating one assistant instead of structuring a system.&lt;/p&gt;
&lt;p&gt;For this setup, the &lt;code&gt;main&lt;/code&gt;/&lt;code&gt;personal&lt;/code&gt; split was just enough structure to feel useful without becoming ceremonial.&lt;/p&gt;
&lt;p&gt;And it set up the final problem in this series perfectly: what happens when a reminder job is created in one conversation but later forgets how to get back there.&lt;/p&gt;
&lt;p&gt;Next in the series: &lt;a class="link" href="https://www.chingono.com/blog/2026/04/04/why-my-openclaw-reminders-werent-reaching-signal-or-teams/" &gt;Why My OpenClaw Reminders Weren&amp;rsquo;t Reaching Signal or Teams&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Inside the Dockerfile Behind My OpenClaw Gateway</title><link>https://www.chingono.com/blog/2026/03/15/inside-the-dockerfile-behind-my-openclaw-gateway/</link><pubDate>Sun, 15 Mar 2026 17:00:00 +0000</pubDate><guid>https://www.chingono.com/blog/2026/03/15/inside-the-dockerfile-behind-my-openclaw-gateway/</guid><description>&lt;img src="https://www.chingono.com/blog/2026/03/15/inside-the-dockerfile-behind-my-openclaw-gateway/cover.png" alt="Featured image of post Inside the Dockerfile Behind My OpenClaw Gateway" /&gt;&lt;p&gt;The first two posts in this series covered the Docker decision and the Signal/Teams channel integration. This one gets into the Dockerfile itself.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not very long, but it&amp;rsquo;s doing more architectural work than its size suggests.&lt;/p&gt;
&lt;h2 id="i-started-from-the-openclaw-base-image-on-purpose"&gt;I started from the OpenClaw base image on purpose
&lt;/h2&gt;&lt;p&gt;The first line matters:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-Dockerfile" data-lang="Dockerfile"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ghcr.io/openclaw/openclaw:latest&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;I didn&amp;rsquo;t want to rebuild OpenClaw from scratch if I didn&amp;rsquo;t have to.&lt;/p&gt;
&lt;p&gt;The base image already gets me the core runtime. What I needed was an opinionated extension of that runtime for my own environment. So this Dockerfile is less &amp;ldquo;build a platform from zero&amp;rdquo; and more &amp;ldquo;declare the exact operational additions my setup needs.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;That distinction keeps the file focused.&lt;/p&gt;
&lt;h2 id="the-version-arguments-make-the-customization-explicit"&gt;The version arguments make the customization explicit
&lt;/h2&gt;&lt;p&gt;Very early in the file I keep a couple of version arguments:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-Dockerfile" data-lang="Dockerfile"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;ARG&lt;/span&gt; &lt;span class="nv"&gt;PNPM_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;9&lt;/span&gt;.15.6&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;ARG&lt;/span&gt; &lt;span class="nv"&gt;SIGNALCLI_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.14.1&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;I do this even when not every argument is fully threaded through every install step yet.&lt;/p&gt;
&lt;p&gt;It makes the additions feel intentional instead of accidental, and it gives me a clean place to pin or update them over time without pretending the whole image is fully static.&lt;/p&gt;
&lt;h2 id="i-keep-extra-node-runtime-pieces-out-of-the-main-install-path"&gt;I keep extra Node runtime pieces out of the main install path
&lt;/h2&gt;&lt;p&gt;This block is one of the more important small decisions in the file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-Dockerfile" data-lang="Dockerfile"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;ENV&lt;/span&gt; &lt;span class="nv"&gt;EXTRA_NODE_MODULES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/home/node/.openclaw-extra/node_modules &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;NODE_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/home/node/.openclaw-extra/node_modules&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;That extra module path is how I keep the runtime additions separated from the base image&amp;rsquo;s own install layout.&lt;/p&gt;
&lt;p&gt;I like this because it keeps things layered:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;base image provides OpenClaw&lt;/li&gt;
&lt;li&gt;extra module path provides my environment-specific additions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It&amp;rsquo;s cleaner than pretending I own the whole upstream install tree.&lt;/p&gt;
&lt;h2 id="the-root-phase-is-for-system-level-capability"&gt;The root phase is for system-level capability
&lt;/h2&gt;&lt;p&gt;After switching to &lt;code&gt;root&lt;/code&gt;, the file installs the system packages the stack actually needs:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-Dockerfile" data-lang="Dockerfile"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt-get install -y --no-install-recommends &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; curl &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; jq &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ca-certificates &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; gpg &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; openssh-client &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; libstdc++6 &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; zlib1g &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; libgcc-s1&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;This section isn&amp;rsquo;t glamorous, but it matters.&lt;/p&gt;
&lt;p&gt;These are the packages that make the runtime actually usable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;curl&lt;/code&gt; and &lt;code&gt;jq&lt;/code&gt; for scripting and diagnostics&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gpg&lt;/code&gt; and certificate tooling for package installs&lt;/li&gt;
&lt;li&gt;&lt;code&gt;openssh-client&lt;/code&gt; because the agent works with repositories and tunnels&lt;/li&gt;
&lt;li&gt;the runtime libraries needed by installed binaries&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I try hard not to let this layer become a junk drawer. If a package is there, I want to be able to explain why it exists.&lt;/p&gt;
&lt;h2 id="github-cli-belongs-in-the-image-because-the-agent-actually-uses-it"&gt;GitHub CLI belongs in the image because the agent actually uses it
&lt;/h2&gt;&lt;p&gt;I also install &lt;code&gt;gh&lt;/code&gt; in the image.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s one of those choices that looks unnecessary until you remember what OpenClaw is actually doing here. It&amp;rsquo;s not just chatting. It&amp;rsquo;s working with repositories, issues, and pull requests. I wanted the GitHub CLI available in the environment the agent actually runs in, not as some optional extra on the host.&lt;/p&gt;
&lt;p&gt;That keeps the runtime consistent between the gateway and the CLI container, which matters.&lt;/p&gt;
&lt;h2 id="signal-cli-is-not-an-afterthought"&gt;Signal CLI is not an afterthought
&lt;/h2&gt;&lt;p&gt;The Signal section is the clearest example of why this had to be a custom image:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-Dockerfile" data-lang="Dockerfile"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;RUN&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt; -eux&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; mkdir -p /etc/apt/keyrings&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; curl -fsSL https://packaging.gitlab.io/signal-cli/gpg.key &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; gpg --dearmor -o /etc/apt/keyrings/signal-cli.gpg&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;deb [signed-by=/etc/apt/keyrings/signal-cli.gpg] https://packaging.gitlab.io/signal-cli signalcli main&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;gt; /etc/apt/sources.list.d/signal-cli.list&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; apt-get update&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; apt-get install -y --no-install-recommends signal-cli-native&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; rm -rf /var/lib/apt/lists/*&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; signal-cli --version&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;I wanted Signal to be a first-class runtime capability, not a hand-installed special case.&lt;/p&gt;
&lt;p&gt;Once it&amp;rsquo;s in the image, I know exactly where it comes from and which container owns it. Then I can mount the Signal state directory separately and let rebuilds stay rebuilds instead of becoming identity-loss events.&lt;/p&gt;
&lt;h2 id="permissions-matter-more-than-people-think"&gt;Permissions matter more than people think
&lt;/h2&gt;&lt;p&gt;Before dropping privileges again, the Dockerfile prepares the runtime directories and fixes ownership for the non-root &lt;code&gt;node&lt;/code&gt; user.&lt;/p&gt;
&lt;p&gt;That kind of step is easy to skip when you&amp;rsquo;re just trying to get it running. It&amp;rsquo;s also the kind of step that bites you later when mounted state or runtime-generated files start colliding with user permissions.&lt;/p&gt;
&lt;p&gt;I would rather be explicit here.&lt;/p&gt;
&lt;h2 id="the-pnpmcorepack-step-has-to-happen-before-switching-users"&gt;The pnpm/corepack step has to happen before switching users
&lt;/h2&gt;&lt;p&gt;This is one of the details that looks small but is operationally important:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-Dockerfile" data-lang="Dockerfile"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;RUN&lt;/span&gt; corepack &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; corepack prepare pnpm@&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PNPM_VERSION&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; --activate&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;I do that while still running as &lt;code&gt;root&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s deliberate. Those paths need write access, and I don&amp;rsquo;t want to debug avoidable permission problems later. This is exactly the kind of detail that turns a Dockerfile from &amp;ldquo;technically valid&amp;rdquo; into &amp;ldquo;actually maintainable.&amp;rdquo;&lt;/p&gt;
&lt;h2 id="i-switch-back-to-node-for-runtime-behavior"&gt;I switch back to &lt;code&gt;node&lt;/code&gt; for runtime behavior
&lt;/h2&gt;&lt;p&gt;Once the system-level setup is done, the file drops back to the non-root user and stays there for the runtime-facing steps.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the right default here. The container needs enough privilege to install what it needs at build time, but it doesn&amp;rsquo;t need to run the application as root.&lt;/p&gt;
&lt;h2 id="the-teams-specific-node-addition-lives-in-the-extra-module-path"&gt;The Teams-specific Node addition lives in the extra module path
&lt;/h2&gt;&lt;p&gt;The final runtime customization is this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-Dockerfile" data-lang="Dockerfile"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;RUN&lt;/span&gt; pnpm add &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --dir /home/node/.openclaw-extra &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --save-exact &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; @microsoft/agents-hosting@1.3.1&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;That one line pretty much summarizes the whole Dockerfile philosophy.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not forking OpenClaw. I&amp;rsquo;m not replacing the base image. I&amp;rsquo;m adding the exact extra runtime capability my environment needs, in a separate path, with a pinned version, after the base runtime is already in place.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the kind of extension model I trust more.&lt;/p&gt;
&lt;h2 id="the-file-is-short-because-i-wanted-it-to-stay-inspectable"&gt;The file is short because I wanted it to stay inspectable
&lt;/h2&gt;&lt;p&gt;I could have packed more into this image.&lt;/p&gt;
&lt;p&gt;I could have added more helpers, more debugging tools, more convenience packages, more &amp;ldquo;while I&amp;rsquo;m here&amp;rdquo; installations.&lt;/p&gt;
&lt;p&gt;I chose not to.&lt;/p&gt;
&lt;p&gt;For this kind of system, I think a Dockerfile should be easy to read top to bottom and answer one question: &lt;strong&gt;what does this runtime need that upstream doesn&amp;rsquo;t already provide?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In my case, the answer was:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a few system tools&lt;/li&gt;
&lt;li&gt;GitHub CLI&lt;/li&gt;
&lt;li&gt;Signal CLI&lt;/li&gt;
&lt;li&gt;an extra Node path&lt;/li&gt;
&lt;li&gt;pnpm-managed Teams hosting support&lt;/li&gt;
&lt;li&gt;correct ownership and runtime defaults&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is enough.&lt;/p&gt;
&lt;p&gt;Honestly, &amp;ldquo;enough&amp;rdquo; is one of the healthiest instincts you can have when building infrastructure for yourself.&lt;/p&gt;
&lt;p&gt;Next in the series: &lt;a class="link" href="https://www.chingono.com/blog/2026/04/03/how-i-split-openclaw-into-main-and-personal-agents/" &gt;How I Split OpenClaw into Main and Personal Agents&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>How I Wired Signal and Microsoft Teams into a Custom OpenClaw Image</title><link>https://www.chingono.com/blog/2026/03/15/how-i-wired-signal-and-microsoft-teams-into-a-custom-openclaw-image/</link><pubDate>Sun, 15 Mar 2026 13:00:00 +0000</pubDate><guid>https://www.chingono.com/blog/2026/03/15/how-i-wired-signal-and-microsoft-teams-into-a-custom-openclaw-image/</guid><description>&lt;img src="https://www.chingono.com/blog/2026/03/15/how-i-wired-signal-and-microsoft-teams-into-a-custom-openclaw-image/cover.png" alt="Featured image of post How I Wired Signal and Microsoft Teams into a Custom OpenClaw Image" /&gt;&lt;p&gt;In the &lt;a class="link" href="https://www.chingono.com/blog/2026/03/05/why-i-run-openclaw-in-docker-on-my-own-machine/" &gt;first post&lt;/a&gt; I explained why I wanted Docker as the foundation. This one is the next practical problem: how to get OpenClaw talking to &lt;strong&gt;Signal&lt;/strong&gt; and &lt;strong&gt;Microsoft Teams&lt;/strong&gt; without turning the host machine into a dependency junk drawer.&lt;/p&gt;
&lt;p&gt;The short version is that I ended up building a custom image because the base runtime got me close, but not all the way there.&lt;/p&gt;
&lt;h2 id="signal-and-teams-were-different-kinds-of-problems"&gt;Signal and Teams were different kinds of problems
&lt;/h2&gt;&lt;p&gt;Signal and Teams pushed on different parts of the system.&lt;/p&gt;
&lt;p&gt;Signal is much more of a runtime-tooling problem.&lt;/p&gt;
&lt;p&gt;You need a working Signal CLI runtime in the container and a persistent place to keep Signal state. If the container can send Signal messages but the identity disappears on rebuild, you have not actually solved the problem.&lt;/p&gt;
&lt;p&gt;Teams is more of a Node/runtime integration problem.&lt;/p&gt;
&lt;p&gt;It needs the right hosting support in the image, the right webhook port exposed, and the right application credentials sitting in OpenClaw config so the bot framework side can talk to Microsoft properly.&lt;/p&gt;
&lt;p&gt;I didn&amp;rsquo;t want to solve those two things in two completely different operational styles.&lt;/p&gt;
&lt;p&gt;So I chose one image and one compose layout that could support both.&lt;/p&gt;
&lt;h2 id="why-i-did-not-stop-at-the-base-openclaw-image"&gt;Why I did not stop at the base OpenClaw image
&lt;/h2&gt;&lt;p&gt;The base OpenClaw image was a good starting point, but I needed more in the environment:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;signal-cli-native&lt;/code&gt; installed in the container&lt;/li&gt;
&lt;li&gt;GitHub CLI and SSH tooling for the agent&amp;rsquo;s repo workflows&lt;/li&gt;
&lt;li&gt;an extra Node modules path for additional runtime packages&lt;/li&gt;
&lt;li&gt;a clean way for both the gateway container and the CLI container to share the same capabilities&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That led to a custom image tagged locally as &lt;code&gt;openclaw-local:teams&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The name reflects where I started operationally, but the important part is not the tag. The important part is that the image became the place where I declared, very explicitly, &amp;ldquo;this is the OpenClaw runtime I actually depend on.&amp;rdquo;&lt;/p&gt;
&lt;h2 id="how-i-handled-signal"&gt;How I handled Signal
&lt;/h2&gt;&lt;p&gt;For Signal, the key decision was to keep the runtime inside the image and the account state outside it.&lt;/p&gt;
&lt;p&gt;The image installs &lt;code&gt;signal-cli-native&lt;/code&gt;, which gives the container the actual tool it needs to send and receive Signal messages.&lt;/p&gt;
&lt;p&gt;Then the compose file mounts the Signal data directory into:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/home/node/.local/share/signal-cli
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;That was the right split for me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the image owns the executable&lt;/li&gt;
&lt;li&gt;the volume owns the durable Signal identity&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This matters more than it sounds.&lt;/p&gt;
&lt;p&gt;If the image knows how to run Signal but the identity is trapped inside a replaceable container layer, every rebuild becomes risky. If the state is mounted and durable, rebuilds are much less dramatic.&lt;/p&gt;
&lt;p&gt;On the OpenClaw side, the Signal channel is configured with pairing and allowlists so the bot is not just open to the world. That let me keep Signal useful without letting it become an uncontrolled ingress point.&lt;/p&gt;
&lt;h2 id="how-i-handled-teams"&gt;How I handled Teams
&lt;/h2&gt;&lt;p&gt;Teams had a different shape.&lt;/p&gt;
&lt;p&gt;The container needed the extra Node package support for the Microsoft hosting layer, and the gateway needed to expose the Teams webhook port. In my setup that means port &lt;code&gt;3978&lt;/code&gt; is published by the gateway container so the remote edge can forward &lt;code&gt;/api/messages&lt;/code&gt; traffic back to the local OpenClaw runtime.&lt;/p&gt;
&lt;p&gt;The actual Teams app credentials live in OpenClaw config, not in the image. That&amp;rsquo;s exactly where I want them.&lt;/p&gt;
&lt;p&gt;The image should describe runtime capability.&lt;/p&gt;
&lt;p&gt;The config should describe environment-specific identity.&lt;/p&gt;
&lt;p&gt;That boundary kept the setup much easier to move, rebuild, and reason about.&lt;/p&gt;
&lt;h2 id="one-image-two-operational-benefits"&gt;One image, two operational benefits
&lt;/h2&gt;&lt;p&gt;Using the same custom image for both &lt;code&gt;gateway&lt;/code&gt; and &lt;code&gt;cli&lt;/code&gt; gave me two benefits I really wanted.&lt;/p&gt;
&lt;p&gt;First, it removed &amp;ldquo;works in one container but not the other&amp;rdquo; drift.&lt;/p&gt;
&lt;p&gt;If the gateway can use the runtime, the CLI can too. If the CLI can inspect or patch something, it is doing so in the same environment the gateway actually uses. I&amp;rsquo;ve learned to value that kind of consistency a lot.&lt;/p&gt;
&lt;p&gt;Second, it let me keep the OpenClaw-specific runtime tweaks in one place:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the extra Node modules path&lt;/li&gt;
&lt;li&gt;the installed system packages&lt;/li&gt;
&lt;li&gt;Signal CLI&lt;/li&gt;
&lt;li&gt;GitHub CLI&lt;/li&gt;
&lt;li&gt;pnpm-prepared extras&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It&amp;rsquo;s a much nicer maintenance story than trying to remember which bits live on the host, which belong to the container, and which only exist in some forgotten shell session.&lt;/p&gt;
&lt;h2 id="the-compose-file-completed-the-picture"&gt;The compose file completed the picture
&lt;/h2&gt;&lt;p&gt;The image by itself was not enough. The compose file is what turned it into a working system.&lt;/p&gt;
&lt;p&gt;That is where I defined the things that make the setup feel real:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;mounted OpenClaw config&lt;/li&gt;
&lt;li&gt;mounted workspaces&lt;/li&gt;
&lt;li&gt;mounted Signal data&lt;/li&gt;
&lt;li&gt;shared access to source repositories&lt;/li&gt;
&lt;li&gt;local Ollama dependency&lt;/li&gt;
&lt;li&gt;editor access&lt;/li&gt;
&lt;li&gt;network sharing between the gateway and CLI&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is one reason I still like Docker Compose for personal infrastructure. It doesn&amp;rsquo;t just run containers. It describes the operating assumptions of the stack in one place.&lt;/p&gt;
&lt;h2 id="the-setup-was-already-hinting-at-the-routing-story"&gt;The setup was already hinting at the routing story
&lt;/h2&gt;&lt;p&gt;Even at this stage, there was a lesson hiding in plain sight: getting the channels working is the easy part. Once one runtime can talk to Signal and Teams, the questions come quickly — which agent answers where, what state belongs to which workspace, how does a background task find its way back to the right chat. That&amp;rsquo;s where the series goes next.&lt;/p&gt;
&lt;p&gt;Next in the series: &lt;a class="link" href="https://www.chingono.com/blog/2026/03/15/inside-the-dockerfile-behind-my-openclaw-gateway/" &gt;Inside the Dockerfile Behind My OpenClaw Gateway&lt;/a&gt;.&lt;/p&gt;</description></item></channel></rss>