<?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: Aviv Laufer</title>
    <description>The latest articles on DEV Community by Aviv Laufer (@avivl).</description>
    <link>https://dev.to/avivl</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F34647%2F9e6962c5-8187-44f5-a7da-a1bd4447cced.png</url>
      <title>DEV Community: Aviv Laufer</title>
      <link>https://dev.to/avivl</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/avivl"/>
    <language>en</language>
    <item>
      <title>Every PR gets an agent review before a human sees it</title>
      <dc:creator>Aviv Laufer</dc:creator>
      <pubDate>Tue, 10 Nov 2026 16:07:14 +0000</pubDate>
      <link>https://dev.to/avivl/every-pr-gets-an-agent-review-before-a-human-sees-it-440h</link>
      <guid>https://dev.to/avivl/every-pr-gets-an-agent-review-before-a-human-sees-it-440h</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Part 4 of My Agentic Setup. &lt;a href="https://dev.to/avivl/i-talk-to-one-agent-it-runs-the-others-26k4"&gt;Start from part 1&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;About 50 PRs a week come to me for review. I don't open any of them cold.&lt;/p&gt;

&lt;p&gt;Each PR gets read first by an agent, in its own pane. The agent can read the PR and nothing else: it can't comment, approve, push or merge. It writes a report on my machine, and I read the report before I open the diff.&lt;/p&gt;

&lt;p&gt;I start all of it with one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fmreview https://github.com/acme/api/pull/123 acme/web:45 &lt;span class="s1"&gt;'acme/api#130'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The idea: every PR gets a review from a model on a different vendor than the one that wrote the code, the reviewer is read-only, and its report comes to me before anything is posted on GitHub.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgj1ox4hc8obiwz563e7r.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgj1ox4hc8obiwz563e7r.png" alt="fmreview takes a list of PRs and hands them to the first mate. The first mate starts one read-only scout per PR, each in its own pane, on the reviewer model. Each scout writes report.md on my machine. The first mate relays the verdicts to me and stops. Only I post anything on GitHub." width="800" height="1210"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The reviewer model didn't reach the reviewers
&lt;/h3&gt;

&lt;p&gt;My main agent, OMP, has a reviewer role pinned to a model on another vendor. But the pin applies only to a subagent whose type is literally &lt;code&gt;reviewer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;My review agents are Firstmate scouts, and a scout is not a &lt;code&gt;reviewer&lt;/code&gt;. Neither are the subagents a scout starts on its own. None of them get the reviewer model by themselves.&lt;/p&gt;

&lt;p&gt;The fix is to pin the model by hand, every time. The note in my captain's preferences:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="k"&gt;- &lt;/span&gt;Dispatch every code-review task on &lt;span class="sb"&gt;`openai-codex/gpt-6.1-sol`&lt;/span&gt; at high effort,
  not the default model (same as omp's &lt;span class="sb"&gt;`task.agentModelOverrides.reviewer`&lt;/span&gt; and
  &lt;span class="sb"&gt;`FMREVIEW_MODEL`&lt;/span&gt;). When spawning the scout, pass
  &lt;span class="sb"&gt;`--model openai-codex/gpt-6.1-sol --effort high`&lt;/span&gt; explicitly (the omp override
  only auto-applies to a subagent literally typed &lt;span class="sb"&gt;`reviewer`&lt;/span&gt;, not to a
  &lt;span class="sb"&gt;`scout`&lt;/span&gt;-typed spawn or its own internal fan-out subagents)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;fmreview&lt;/code&gt; puts the model into the message it sends, so the first mate doesn't have to remember:&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="c"&gt;# Keep FMREVIEW_MODEL/EFFORT in sync with&lt;/span&gt;
&lt;span class="c"&gt;# task.agentModelOverrides.reviewer in ~/.omp/agent/config.yml.&lt;/span&gt;
: &lt;span class="nv"&gt;${FMREVIEW_HARNESS:="omp"}&lt;/span&gt;
: &lt;span class="nv"&gt;${FMREVIEW_MODEL:="openai-codex/gpt-6.1-sol"}&lt;/span&gt;
: &lt;span class="nv"&gt;${FMREVIEW_EFFORT:="high"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is also an option to start omp in the reviewer role from the command line, if your omp build supports it. I leave it empty. Pinning the model is the path I verified.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;#&lt;/code&gt; is a glob in my shell
&lt;/h3&gt;

&lt;p&gt;The short way to name a PR is &lt;code&gt;acme/api#123&lt;/code&gt;. In zsh with &lt;code&gt;extendedglob&lt;/code&gt; on, &lt;code&gt;#&lt;/code&gt; is a glob operator, so that word has to be quoted, or zsh tries to match files with it.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;fmreview&lt;/code&gt; also takes a form that needs no quotes:&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="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"$t"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
  https://github.com/*/pull/*&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;t&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"${t#https://github.com/}"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s#%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"${t%%/pull/*}"&lt;/span&gt; &lt;span class="s2"&gt;"${t##*/pull/}"&lt;/span&gt; &lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
  */*\#&lt;span class="o"&gt;[&lt;/span&gt;0-9&lt;span class="o"&gt;]&lt;/span&gt;*&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"$t"&lt;/span&gt; &lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
  &lt;span class="c"&gt;# owner/repo:123 needs no quoting under extendedglob, unlike the # form.&lt;/span&gt;
  */*:&lt;span class="o"&gt;[&lt;/span&gt;0-9&lt;span class="o"&gt;]&lt;/span&gt;*&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s#%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"${t%%:*}"&lt;/span&gt; &lt;span class="s2"&gt;"${t##*:}"&lt;/span&gt; &lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt;0-9&lt;span class="o"&gt;]&lt;/span&gt;*&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; -n &lt;span class="s2"&gt;"$dr"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s#%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"$dr"&lt;/span&gt; &lt;span class="s2"&gt;"$t"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;return&lt;/span&gt; 0 &lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
  *&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;return&lt;/span&gt; 1 &lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;esac&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A bare number works too, with &lt;code&gt;--repo acme/api&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  herdr's split ratio is the other way around
&lt;/h3&gt;

&lt;p&gt;Before &lt;code&gt;fmreview&lt;/code&gt; I had a simpler batch launcher: one pane per PR, each running a review. The first version was for cmux, which takes a whole pane layout as JSON in one call. When I moved to herdr, there was no such call. Panes are made one &lt;code&gt;herdr pane split&lt;/code&gt; at a time.&lt;/p&gt;

&lt;p&gt;To get equal panes, you split off one slice at a time. herdr's &lt;code&gt;--ratio&lt;/code&gt; is the share the &lt;strong&gt;original&lt;/strong&gt; pane keeps, not the share the new pane gets. Splitting a 20-row pane with &lt;code&gt;--ratio 0.75&lt;/code&gt; leaves it 15 rows and gives 5 away. So to carve &lt;code&gt;n&lt;/code&gt; equal slices, the ratio at each step is &lt;code&gt;1/n&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;split_off&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  herdr pane split &lt;span class="s2"&gt;"$1"&lt;/span&gt; --direction &lt;span class="s2"&gt;"$2"&lt;/span&gt; \
    --ratio &lt;span class="s2"&gt;"$(awk "&lt;/span&gt;BEGIN &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; \&lt;span class="s2"&gt;"%.6f\", 1 / $3 }"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;" --no-focus |
    json_get result.pane.pane_id
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The panes are laid out as a grid, rows first, so the PRs read left to right and top to bottom, and the panes stay close to square. Five PRs make three panes on top and two below.&lt;/p&gt;

&lt;h3&gt;
  
  
  A running server that looked down
&lt;/h3&gt;

&lt;p&gt;The launcher checks that a herdr server is running. The obvious check is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;herdr status server &lt;span class="o"&gt;|&lt;/span&gt; grep -q &lt;span class="s2"&gt;"status: running"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;set -o pipefail&lt;/code&gt;, this fails even when the server is up. &lt;code&gt;grep -q&lt;/code&gt; exits on the first match and closes the pipe. herdr is still writing, gets SIGPIPE, and exits with an error. pipefail reports that error as the result of the whole pipeline.&lt;/p&gt;

&lt;p&gt;The fix is to capture first and match after:&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="nv"&gt;server_status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"$(herdr status server 2&amp;gt;/dev/null || true)"&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"$server_status"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
*&lt;span class="s2"&gt;"status: running"&lt;/span&gt;*&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
*&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Error: no herdr server is running. Start one with: herdr"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;amp;&lt;/span&gt;2&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; 1 &lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;esac&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  One PR: review-pr
&lt;/h3&gt;

&lt;p&gt;The smallest piece is &lt;code&gt;review-pr&lt;/code&gt;. It takes a PR URL, goes to my clone of that repo, and starts Cursor's agent CLI with the &lt;code&gt;/review-pr&lt;/code&gt; command:&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="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"$PR_URL"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ github\.com/&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;^/&lt;span class="o"&gt;]&lt;/span&gt;+&lt;span class="o"&gt;)&lt;/span&gt;/&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;^/&lt;span class="o"&gt;]&lt;/span&gt;+&lt;span class="o"&gt;)&lt;/span&gt;/pull/&lt;span class="o"&gt;[&lt;/span&gt;0-9&lt;span class="o"&gt;]&lt;/span&gt;+ &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="nv"&gt;ORG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"${BASH_REMATCH[1]}"&lt;/span&gt;
    &lt;span class="nv"&gt;REPO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"${BASH_REMATCH[2]}"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="nv"&gt;REPO_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"$HOME/GitHub/$ORG/$REPO"&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"$REPO_DIR"&lt;/span&gt;
&lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;"$(dirname "&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt;")/wt-agent"&lt;/span&gt; agent &lt;span class="s2"&gt;"/review-pr $PR_URL"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It needs the repo cloned at &lt;code&gt;~/GitHub/&amp;lt;org&amp;gt;/&amp;lt;repo&amp;gt;&lt;/code&gt;, and it stops if the folder isn't there. &lt;code&gt;/review-pr&lt;/code&gt; is the command from the &lt;code&gt;pr-review-toolkit&lt;/code&gt; plugin. &lt;code&gt;wt-agent&lt;/code&gt; sets a marker in &lt;code&gt;wt list&lt;/code&gt; while the agent runs, so I can see which worktrees have an agent in them.&lt;/p&gt;

&lt;p&gt;The review stays in the pane. Nothing is posted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Many PRs: one scout each
&lt;/h3&gt;

&lt;p&gt;For a batch, I use &lt;code&gt;fmreview&lt;/code&gt;. It does four things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Normalizes each PR to &lt;code&gt;owner/repo#number&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Creates a Firstmate home called &lt;code&gt;reviews&lt;/code&gt;, the same way &lt;code&gt;fm&lt;/code&gt; does, if it doesn't exist.&lt;/li&gt;
&lt;li&gt;Writes a review charter into that home.&lt;/li&gt;
&lt;li&gt;Opens the home in herdr, waits for omp to start, and sends the first mate this message:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ahoy

Dispatch one SCOUT per pull request below. One scout each, in parallel, each in its own pane.

Pull requests:
- acme/api PR #123
- acme/web PR #45

Profile for every one of these scouts: harness omp, model openai-codex/gpt-6.1-sol, effort high.

Register each repo as local-only if it is not registered yet.

Every brief must carry the full contents of data/review-charter.md in its Firstmate spec.
The charter is binding: these scouts are read-only and must not comment, review, approve,
request changes, push, or merge anything on GitHub.

When a scout finishes, relay its verdict and findings to me and then STOP. Do not act on
any recommendation, do not open follow-up tasks, and do not tear the scout down until I say so.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;local-only&lt;/code&gt; means the first mate never opens a PR for that repo. A scout writes a report, never code.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;fmreview --print&lt;/code&gt; shows the message without sending it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The charter
&lt;/h3&gt;

&lt;p&gt;Every scout gets the same charter, word for word, in its brief. The first part is about what it may not do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Read-only, without exception
&lt;/span&gt;&lt;span class="k"&gt;- &lt;/span&gt;NEVER post a comment, review, approval, change request, or reaction on GitHub.
&lt;span class="k"&gt;- &lt;/span&gt;NEVER push, close, reopen, label, assign, or merge anything.
&lt;span class="k"&gt;- &lt;/span&gt;NEVER edit files in the project clone or the worktree.
&lt;span class="k"&gt;- &lt;/span&gt;&lt;span class="sb"&gt;`gh`&lt;/span&gt; is for READING the PR only: diff, files, checks, linked issues.
&lt;span class="k"&gt;- &lt;/span&gt;The single deliverable is &lt;span class="sb"&gt;`data/&amp;lt;id&amp;gt;/report.md`&lt;/span&gt;. Nothing leaves the machine.
&lt;span class="k"&gt;- &lt;/span&gt;If the review implies an action, write the recommendation in the report and
  stop. The captain decides every next step.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second part is what the report has:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="k"&gt;- &lt;/span&gt;Verdict in one line, then findings ordered by severity.
&lt;span class="k"&gt;- &lt;/span&gt;Every finding cites &lt;span class="sb"&gt;`path/to/file:line`&lt;/span&gt; from the PR's own diff.
&lt;span class="k"&gt;- &lt;/span&gt;Separate what the diff does from what it fails to do.
&lt;span class="k"&gt;- &lt;/span&gt;Call out failing or missing CI checks and say what they imply.
&lt;span class="k"&gt;- &lt;/span&gt;Flag anything touching auth, tenancy boundaries, data retention, migration
  ordering, or IAM as captain-escalation regardless of test status.
&lt;span class="k"&gt;- &lt;/span&gt;End with a short list of open questions for the author.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The third part says which review tools win when they disagree. First, the repo's own review rules, when a repo has them. A repo with compliance rules, for example, gets those checked before anything generic. Then &lt;code&gt;pr-review-toolkit&lt;/code&gt; for the mechanics: correctness, edge cases, error handling, tests for the changed lines. Then a second mechanical pass, only on what the first two didn't cover.&lt;/p&gt;

&lt;p&gt;Each finding goes in the report once, under the tool with the highest precedence that found it. The report also says which tools were there and which weren't. If none loaded, it says so and reviews without them, instead of guessing what their checklists say.&lt;/p&gt;

&lt;h3&gt;
  
  
  Five passes, one of them for the spec
&lt;/h3&gt;

&lt;p&gt;Inside each review there are five review passes, all on the reviewer model. Four look at the code. The fifth is a spec reviewer, and it gets less context on purpose: only the diff, the commit list, and the linked ticket. When there's no ticket, it gets the intent from the brief.&lt;/p&gt;

&lt;p&gt;It returns three lists, and every item quotes its source line:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;asked but not built&lt;/li&gt;
&lt;li&gt;built but not asked for&lt;/li&gt;
&lt;li&gt;built wrong&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The other four passes read the code the way the author meant it. This one checks the code against what was asked.&lt;/p&gt;

&lt;h3&gt;
  
  
  The reviewer is on a different vendor
&lt;/h3&gt;

&lt;p&gt;The model that writes most of my code is Claude. The reviewer is a GPT model, so the reviewer is never the same model that wrote the code.&lt;/p&gt;

&lt;p&gt;The profiles in my dotfiles set this per machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Agent pins (task.agentModelOverrides). The pr-review-toolkit agents are the
# cheap first pass; reviewer and security-reviewer are the real gate.
&lt;/span&gt;&lt;span class="na"&gt;reviewer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"openai-codex/gpt-6-sol:high"&lt;/span&gt;
&lt;span class="na"&gt;security-reviewer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"anthropic/claude-sonnet-5-5:high"&lt;/span&gt;
&lt;span class="na"&gt;code-reviewer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"@crew-light"&lt;/span&gt;
&lt;span class="na"&gt;silent-failure-hunter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"@crew-light"&lt;/span&gt;
&lt;span class="na"&gt;pr-test-analyzer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"@crew-light"&lt;/span&gt;
&lt;span class="na"&gt;comment-analyzer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"@crew-light"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@crew-light&lt;/code&gt; points at another role, a small model. The &lt;code&gt;pr-review-toolkit&lt;/code&gt; agents run often, so they get the cheap model. The security reviewer is Claude, so only the main reviewer is on the other vendor.&lt;/p&gt;

&lt;p&gt;What runs today:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Work laptop&lt;/th&gt;
&lt;th&gt;Personal laptop&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;default, crew&lt;/td&gt;
&lt;td&gt;writes the code&lt;/td&gt;
&lt;td&gt;Claude Sonnet 5.5&lt;/td&gt;
&lt;td&gt;Claude Sonnet 5.5 via Cursor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reviewer&lt;/td&gt;
&lt;td&gt;the real gate, &lt;code&gt;fmreview&lt;/code&gt; scouts&lt;/td&gt;
&lt;td&gt;GPT-6.1 Sol, high&lt;/td&gt;
&lt;td&gt;GPT-5.6 Terra via Cursor, high&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;security-reviewer&lt;/td&gt;
&lt;td&gt;security pass&lt;/td&gt;
&lt;td&gt;Claude Sonnet 5.5, high&lt;/td&gt;
&lt;td&gt;Claude Sonnet 5.5 via Cursor, high&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pr-review-toolkit agents&lt;/td&gt;
&lt;td&gt;cheap first pass&lt;/td&gt;
&lt;td&gt;Claude Haiku 5.5&lt;/td&gt;
&lt;td&gt;Claude Haiku 5.5 via Cursor&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  When my own agents open the PR
&lt;/h3&gt;

&lt;p&gt;The same review runs on the other side, when my agents write the code. These rules are in the instructions every agent gets (trimmed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="k"&gt;- &lt;/span&gt;Before opening: run the review skill (the repo's own, or
  &lt;span class="sb"&gt;`pr-review-toolkit:review-pr`&lt;/span&gt; otherwise) and &lt;span class="sb"&gt;`simplify`&lt;/span&gt;, and address the findings.
&lt;span class="k"&gt;- &lt;/span&gt;Always draft (&lt;span class="sb"&gt;`gh pr create --draft`&lt;/span&gt;).
&lt;span class="k"&gt;- &lt;/span&gt;Promote to ready only on the captain's explicit instruction for that PR. Green
  CI or self-review never authorizes promoting or merging.
  Humans approve and merge (squash); do not merge unless asked.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So an agent's PR has been reviewed by an agent before it exists, and a human still decides when it's ready and when it merges.&lt;/p&gt;

&lt;h2&gt;
  
  
  A budget for review rounds
&lt;/h2&gt;

&lt;p&gt;The newest change is a limit on how many times a PR goes back and forth with a reviewer. Until late October, nothing in my setup said when to stop. Now my captain's preferences have this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;### Review budget
&lt;/span&gt;&lt;span class="k"&gt;- &lt;/span&gt;Default to 3 review rounds per PR. Round 1 is a full review. Later rounds
  check only the changed code and the findings that are still open.
&lt;span class="k"&gt;- &lt;/span&gt;Extend to 5 rounds only when the change touches security, permissions, auth,
  data loss, or state and concurrency, and a blocker is still open with
  evidence. Also extend when the captain asks.
&lt;span class="k"&gt;- &lt;/span&gt;Stop early when a round returns only nits.
&lt;span class="k"&gt;- &lt;/span&gt;Keep a ledger of every finding: id, severity, and outcome (fixed, rejected
  with reason, or open). Never guess usage or token numbers. Report only what
  the tools show.
&lt;span class="k"&gt;- &lt;/span&gt;Make the smallest change that satisfies a reviewer. Do not widen the scope
  to fix things nobody flagged.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs7ouwp95bwdmij6p4cbo.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs7ouwp95bwdmij6p4cbo.png" alt="Round 1 is a full review. Rounds 2 and 3 check only the changed code and the open findings. A round with only nits stops early. Rounds 4 and 5 happen only when a blocker is still open with evidence on security, permissions, auth, data loss, or state and concurrency, or when I ask. Every finding goes into a ledger as fixed, rejected with a reason, or open." width="800" height="953"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With the ledger, every finding ends with an outcome, and "rejected" needs a reason. A finding can't just disappear.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The companion folder has sanitized copies of &lt;code&gt;review-pr&lt;/code&gt; and the herdr batch launcher, the PR parser, a script that prints the &lt;code&gt;fmreview&lt;/code&gt; message, and the charter. The tests use a fake agent CLI and a fake herdr that keeps the size and place of every pane, so they check the real grid for 1 to 7 PRs, not just the calls. They also check URL quoting, PR parsing, and the read-only lines in the message.&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;cd&lt;/span&gt; 04-pr-review
make &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/avivl/golem-notes-examples/tree/main/04-pr-review" rel="noopener noreferrer"&gt;golem-notes-examples/04-pr-review&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Who reviews your agents' code, and does the reviewer come from the same vendor as the author? Leave a comment.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is part 4 of my series on running a real agentic dev setup. I publish each part first on Golem Notes, with the full write-up and what's coming next. Subscribe there to get the next one in your inbox: &lt;a href="https://golemnotes.substack.com/p/every-pr-gets-an-agent-review?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=agentic-setup-04" rel="noopener noreferrer"&gt;Golem Notes&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>codereview</category>
      <category>devtools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>My agents on two laptops, controlled from one phone app</title>
      <dc:creator>Aviv Laufer</dc:creator>
      <pubDate>Wed, 28 Oct 2026 05:17:43 +0000</pubDate>
      <link>https://dev.to/avivl/my-agents-on-two-laptops-controlled-from-one-phone-app-35hl</link>
      <guid>https://dev.to/avivl/my-agents-on-two-laptops-controlled-from-one-phone-app-35hl</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Part 3 of My Agentic Setup. &lt;a href="https://dev.to/avivl/i-talk-to-one-agent-it-runs-the-others-26k4"&gt;Start from part 1&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;I have two laptops, a work laptop and a home laptop. Both run herdr, and both have agents working in it. I sit in front of one of them. The agents on the other one keep going, and sometimes they stop and wait for me: a question, an approval, a "done, what next?".&lt;/p&gt;

&lt;p&gt;I wanted three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;See and answer the agents on both laptops from my phone.&lt;/li&gt;
&lt;li&gt;Get a shell on the home laptop from the work laptop.&lt;/li&gt;
&lt;li&gt;The work laptop holds work credentials, so it accepts no incoming connections. Not from the phone, not from the home laptop, not from anywhere.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The idea: each laptop runs its own relay behind its own permanent address, both relays point the phone at the same app, so one app shows the agents from both laptops, and nothing on either laptop listens on the internet.&lt;/p&gt;

&lt;p&gt;The relay was the easy part. The domain, the app's address and the closed lid were not.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjy0ojj2cre92ryy1m4hi.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjy0ojj2cre92ryy1m4hi.png" alt="The phone app talks to two relays. Each laptop dials out to Cloudflare through its own tunnel, so neither one opens a port. The work laptop's relay also serves the phone app, and the home laptop's relay joins that same app." width="800" height="1045"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I use
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;From&lt;/th&gt;
&lt;th&gt;To&lt;/th&gt;
&lt;th&gt;How&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Phone&lt;/td&gt;
&lt;td&gt;both laptops&lt;/td&gt;
&lt;td&gt;herdr-mobile-relay over a Cloudflare tunnel&lt;/td&gt;
&lt;td&gt;every day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Work laptop&lt;/td&gt;
&lt;td&gt;home laptop&lt;/td&gt;
&lt;td&gt;SSH over Tailscale, then &lt;code&gt;hr-home&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;every day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Work laptop&lt;/td&gt;
&lt;td&gt;home laptop&lt;/td&gt;
&lt;td&gt;SSH over Cloudflare Access&lt;/td&gt;
&lt;td&gt;the setup with no VPN on the work laptop&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The phone app is &lt;a href="https://github.com/0cv/herdr-mobile-relay" rel="noopener noreferrer"&gt;herdr-mobile-relay&lt;/a&gt;, a herdr plugin by 0cv. It has its own docs and a setup wizard, so I won't repeat them here. This post is about my setup around it, and what broke.&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke
&lt;/h2&gt;

&lt;h3&gt;
  
  
  I couldn't use a domain I already had
&lt;/h3&gt;

&lt;p&gt;I wanted a permanent hostname per laptop, so I used the relay's stable tunnel option. That runs on a Cloudflare tunnel, and a Cloudflare tunnel needs the domain's DNS hosted on Cloudflare.&lt;/p&gt;

&lt;p&gt;The catch: below the Enterprise plan, Cloudflare can't take over just one subdomain. It takes the whole zone. My existing domain had its DNS somewhere else, with my website and email forwarding on it. Moving all of that just to get one hostname for a relay didn't make sense.&lt;/p&gt;

&lt;p&gt;So I bought a new domain on Cloudflare Registrar. It starts on Cloudflare DNS, so there is nothing to move, and the wizard creates the relay records by itself. Nothing else lives on that domain. In this post it's &lt;code&gt;example.dev&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Before running the wizard, check that the zone is live. It can take up to an hour after the purchase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig NS example.dev +short     &lt;span class="c"&gt;# two *.ns.cloudflare.com names&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The second laptop made a second app
&lt;/h3&gt;

&lt;p&gt;The wizard asks two questions that look the same and aren't.&lt;/p&gt;

&lt;p&gt;The first is the hostname. That is the laptop's own address, and each laptop gets its own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;work laptop: &lt;code&gt;work-relay.example.dev&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;home laptop: &lt;code&gt;home-relay.example.dev&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The second is "Where should the phone setup link open?". This one is not about the laptop. It picks where the &lt;strong&gt;phone app&lt;/strong&gt; is served from.&lt;/p&gt;

&lt;p&gt;That matters on iOS, because iOS ties a home-screen app to the address it was installed from. If each laptop serves its own app, the phone gets two separate Herdr apps, each with one laptop in it. That is the opposite of what I wanted.&lt;/p&gt;

&lt;p&gt;So the two laptops answer differently:&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;Work laptop&lt;/th&gt;
&lt;th&gt;Home laptop&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Laptop's own address&lt;/td&gt;
&lt;td&gt;&lt;code&gt;work-relay.example.dev&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;home-relay.example.dev&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Answer to "where should the phone setup link open?"&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1. This relay&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;2. An existing installed Herdr app&lt;/strong&gt;, then &lt;code&gt;https://work-relay.example.dev&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where the phone app lives&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;work-relay.example.dev&lt;/code&gt; (creates it)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;work-relay.example.dev&lt;/code&gt; (joins it)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Then, on the phone, import the home laptop's invite from &lt;strong&gt;inside&lt;/strong&gt; the installed app, not with the camera. The app then shows &lt;strong&gt;2/2 relays&lt;/strong&gt;, with agents from both laptops in one list.&lt;/p&gt;

&lt;h3&gt;
  
  
  The phone app lives on one laptop
&lt;/h3&gt;

&lt;p&gt;That answer has a cost. The work laptop serves the phone app, so a fresh install of the app, or an app update, needs the work laptop awake and online. The relay setup menu has an option, &lt;strong&gt;Configure App Deployment&lt;/strong&gt;, that can move the app to Cloudflare Pages later and remove that dependency. Run it on one laptop only, the one that publishes app updates.&lt;/p&gt;

&lt;p&gt;One more thing not to do: don't put Cloudflare Access in front of the relay hostnames. The relay already accepts only paired devices, and Access blocks the phone app.&lt;/p&gt;

&lt;h3&gt;
  
  
  Closing the lid still puts the Mac to sleep
&lt;/h3&gt;

&lt;p&gt;A sleeping laptop can't be reached. Not by the relay, not over SSH.&lt;/p&gt;

&lt;p&gt;My first setting for the home laptop was the usual one:&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;sudo &lt;/span&gt;pmset &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;0          &lt;span class="c"&gt;# never idle-sleep on the charger&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That only stops idle sleep. Closing the lid is a separate trigger. macOS ignores the lid only in clamshell mode, and clamshell mode needs three things: the charger, an external display, and an external keyboard or mouse. Without a display, the power log shows a sleep like this, even with &lt;code&gt;sleep 0&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Entering Sleep state due to 'Clamshell Sleep' ... Using AC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My home laptop is a MacBook Pro with an HDMI port, so the fix was an HDMI dummy plug, about $10. It tells macOS there is a monitor. With the plug in and the charger on, the Mac stays awake with the lid closed. Apple documents a keyboard or mouse as required too, but on this Mac it wasn't needed.&lt;/p&gt;

&lt;p&gt;I tested it overnight. The lid was closed for about 16.5 hours, with the plug and no mouse. The best check is not the power log, which is huge and noisy. It is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sysctl kern.sleeptime kern.waketime
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If both times are from before you closed the lid, the Mac didn't sleep. Mine still showed a sleep from before the plug went in.&lt;/p&gt;

&lt;p&gt;Two things that didn't help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sudo pmset -c disablesleep 1&lt;/code&gt; keeps it awake with the lid closed, but on my Mac it didn't stay limited to the charger. It showed up as a global setting, so a closed Mac on battery would stay awake too. I turned it back off.&lt;/li&gt;
&lt;li&gt;Herdr.app has a coffee cup in its status bar that keeps the Mac awake, for example "until agents finish". It only stops idle sleep. Closing the lid still sleeps the Mac.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full AC settings on the home laptop:&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;sudo &lt;/span&gt;pmset &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;0          &lt;span class="c"&gt;# never idle-sleep on the charger&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;pmset &lt;span class="nt"&gt;-c&lt;/span&gt; disksleep 0
&lt;span class="nb"&gt;sudo &lt;/span&gt;pmset &lt;span class="nt"&gt;-c&lt;/span&gt; womp 1           &lt;span class="c"&gt;# wake on network access&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;pmset &lt;span class="nt"&gt;-c&lt;/span&gt; displaysleep 15  &lt;span class="c"&gt;# the screen may still turn off&lt;/span&gt;
pmset &lt;span class="nt"&gt;-g&lt;/span&gt; custom                &lt;span class="c"&gt;# check the "AC Power" section&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-c&lt;/code&gt; is charger only. Battery stays at its defaults.&lt;/p&gt;

&lt;p&gt;One case no sleep setting fixes: with FileVault on, after a reboot the Mac waits at the login screen until someone unlocks it in person.&lt;/p&gt;

&lt;h3&gt;
  
  
  A warning about pairing on iOS
&lt;/h3&gt;

&lt;p&gt;This is from the setup docs, and it's easy to get wrong. On iOS, the home-screen app and the browser keep separate storage. So if you scan the pairing QR and pair in the browser tab, you paired the tab, not the app.&lt;/p&gt;

&lt;p&gt;What to do instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scan the QR with the camera. The page says "Add Herdr to the Home Screen". Don't tap "Pair this browser instead".&lt;/li&gt;
&lt;li&gt;Add it to the home screen, then open it from the home-screen icon.&lt;/li&gt;
&lt;li&gt;If the app says "refused this device", the one-use invite was already used by the browser tab. Print a fresh one on the laptop (command below) and import it inside the app.&lt;/li&gt;
&lt;li&gt;In the app, open &lt;strong&gt;gear &amp;gt; Devices&lt;/strong&gt;, rename this device, and revoke any extra entry the browser tab left behind.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A fresh invite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;herdr plugin action invoke setup-link &lt;span class="nt"&gt;--plugin&lt;/span&gt; herdr-mobile-relay.events
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  One relay per laptop
&lt;/h3&gt;

&lt;p&gt;On each laptop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;herdr &lt;span class="nt"&gt;--version&lt;/span&gt;                                   &lt;span class="c"&gt;# 0.7.5 or newer&lt;/span&gt;
herdr plugin &lt;span class="nb"&gt;install &lt;/span&gt;0cv/herdr-mobile-relay
herdr plugin action invoke install-service &lt;span class="nt"&gt;--plugin&lt;/span&gt; herdr-mobile-relay.events
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last command is the stable tunnel wizard. It opens a browser for the Cloudflare login, asks for the hostname and the app question above, then creates the tunnel, the DNS record and a background service, and prints the pairing QR.&lt;/p&gt;

&lt;p&gt;The login writes &lt;code&gt;~/.cloudflared/cert.pem&lt;/code&gt;. That file is a credential. It never goes into dotfiles.&lt;/p&gt;

&lt;p&gt;To check a relay:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;herdr plugin action invoke status &lt;span class="nt"&gt;--plugin&lt;/span&gt; herdr-mobile-relay.events
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://127.0.0.1:8375/readyz &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code}\n'&lt;/span&gt;   &lt;span class="c"&gt;# 200 when ready&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both laptops dial out to Cloudflare. No port is open on either one, and no router is touched. Traffic through the relay is end-to-end encrypted, so Cloudflare sees timing and frame sizes, not prompts or terminal output.&lt;/p&gt;

&lt;h3&gt;
  
  
  Devices and roles
&lt;/h3&gt;

&lt;p&gt;My phone is the only &lt;strong&gt;controller&lt;/strong&gt;. A controller can type, answer approvals, and start and stop agents. Any other device is a &lt;strong&gt;reader&lt;/strong&gt;: it can watch, but it can't type, approve or upload.&lt;/p&gt;

&lt;p&gt;In the app's settings, &lt;strong&gt;device lock&lt;/strong&gt; is on, so it checks the device when it opens and when it comes back.&lt;/p&gt;

&lt;p&gt;The pairing QR and link are passwords. Each one holds a one-use invite that expires after ten minutes. Don't screenshot it and don't paste it anywhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  The phone and Firstmate
&lt;/h3&gt;

&lt;p&gt;Firstmate drives the same herdr sessions. From the phone, I watch the crew and I work in my own sessions. I don't type into a Firstmate worker's pane: Firstmate treats that as me stepping in, and fixes it up on its next check.&lt;/p&gt;

&lt;h2&gt;
  
  
  SSH from the work laptop
&lt;/h2&gt;

&lt;p&gt;The phone is for watching and answering. For shell work on the home laptop, I SSH in from the work laptop. I set this up two ways, and I use Tailscale day to day.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkudi1zrmht7ox7jelg9d.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkudi1zrmht7ox7jelg9d.png" alt="The work laptop reaches the home laptop's SSH in two ways: over Tailscale, where the policy allows only port 22 on the machine tagged tag:home, and over Cloudflare Access, where the home laptop dials out and the work laptop's ssh goes through cloudflared. Nothing reaches the work laptop: no SSH server, Shields Up on, and the policy denies it." width="800" height="1043"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Tailscale: three locks on the work laptop
&lt;/h3&gt;

&lt;p&gt;All three devices, both laptops and the phone, are on one personal Tailscale network. The default Tailscale policy lets every device reach every other device. I replaced it with this:&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;"tagOwners"&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;"tag:home"&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;"autogroup:admin"&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;"acls"&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="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"accept"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"src"&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;"autogroup:member"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"dst"&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;"tag:home:22"&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;My devices can reach port 22 on machines tagged &lt;code&gt;tag:home&lt;/code&gt;. Nothing else is allowed. Only the home laptop has that tag. A tagged device also has key expiry turned off, so it doesn't drop off the network every 180 days.&lt;/p&gt;

&lt;p&gt;That is one lock on the work laptop. There are two more:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No SSH server.&lt;/strong&gt; Remote Login stays off in System Settings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shields Up.&lt;/strong&gt; The Tailscale app refuses incoming connections on this device.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tailscale &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;--shields-up&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;tailscale debug prefs | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; shields      &lt;span class="c"&gt;# "ShieldsUp": true&lt;/span&gt;
netstat &lt;span class="nt"&gt;-an&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; tcp | &lt;span class="nb"&gt;grep &lt;/span&gt;LISTEN | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'\.22 '&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"no ssh server"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the check that matters, from the home laptop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nc &lt;span class="nt"&gt;-vz&lt;/span&gt; &lt;span class="nt"&gt;-G&lt;/span&gt; 3 &amp;lt;work-laptop&amp;gt; 22      &lt;span class="c"&gt;# must fail&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tailscale ping&lt;/code&gt; may still answer. Ping isn't covered by the policy.&lt;/p&gt;

&lt;p&gt;Tailscale's own SSH server isn't available in the macOS app, so the home laptop uses the Mac's normal OpenSSH, reached only over Tailscale. The SSH config entry on the work laptop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;Host&lt;/span&gt; home-ts
    &lt;span class="k"&gt;HostName&lt;/span&gt; home-laptop
    &lt;span class="k"&gt;IdentityFile&lt;/span&gt; ~/.ssh/id_ed25519_&amp;lt;this-machine&amp;gt;
    &lt;span class="k"&gt;ServerAliveInterval&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One key per machine, generated on that machine. After &lt;code&gt;ssh-copy-id&lt;/code&gt; works, password logins on the home laptop are turned off:&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;printf&lt;/span&gt; &lt;span class="s1"&gt;'PasswordAuthentication no\nKbdInteractiveAuthentication no\n'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/ssh/sshd_config.d/100-keys-only.conf
&lt;span class="nb"&gt;sudo &lt;/span&gt;launchctl kickstart &lt;span class="nt"&gt;-k&lt;/span&gt; system/com.openssh.sshd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep that session open and test a new &lt;code&gt;ssh&lt;/code&gt; before you close it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloudflare Access: no VPN on the work laptop
&lt;/h3&gt;

&lt;p&gt;The other way uses no VPN on the work laptop at all. The home laptop runs a second &lt;code&gt;cloudflared&lt;/code&gt; tunnel, for SSH only, separate from the relay's tunnel. The work laptop's &lt;code&gt;ssh&lt;/code&gt; goes through &lt;code&gt;cloudflared access ssh&lt;/code&gt;, which only makes outgoing connections:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;Host&lt;/span&gt; home
    &lt;span class="k"&gt;HostName&lt;/span&gt; ssh-home.example.dev
    &lt;span class="k"&gt;IdentityFile&lt;/span&gt; ~/.ssh/id_ed25519_&amp;lt;this-machine&amp;gt;
    &lt;span class="k"&gt;ProxyCommand&lt;/span&gt; /opt/homebrew/bin/cloudflared access ssh --hostname %h
    &lt;span class="k"&gt;ServerAliveInterval&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An Access application in front of &lt;code&gt;ssh-home.example.dev&lt;/code&gt; allows one email. So the first &lt;code&gt;ssh home&lt;/code&gt; opens a browser login with a code sent to that email, and only then does the SSH key check happen. Two checks before a shell.&lt;/p&gt;

&lt;h3&gt;
  
  
  Joining the running agents
&lt;/h3&gt;

&lt;p&gt;herdr keeps a server running on the home laptop. Its sessions, and the agents in its panes, keep running whether anyone is attached or not. Attaching adds another view of the same panes.&lt;/p&gt;

&lt;p&gt;herdr can attach over SSH by itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;herdr &lt;span class="nt"&gt;--remote&lt;/span&gt; home-ts                     &lt;span class="c"&gt;# the default session&lt;/span&gt;
herdr &lt;span class="nt"&gt;--remote&lt;/span&gt; home-ts &lt;span class="nt"&gt;--session&lt;/span&gt; agents
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shortcut for that is &lt;code&gt;hr-home&lt;/code&gt;. Detach with Ctrl+B, then Q.&lt;/p&gt;

&lt;p&gt;A few rules I keep:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The agent in a pane is the same process. I type into its pane. I never start &lt;code&gt;omp --resume&lt;/code&gt; on a conversation that is already open in a pane, because that runs a second process on the same chat.&lt;/li&gt;
&lt;li&gt;Closing SSH only closes my view. The agents keep running.&lt;/li&gt;
&lt;li&gt;A smaller terminal resizes the shared panes while it's attached. They go back when it detaches.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  A remote shell should look remote
&lt;/h3&gt;

&lt;p&gt;So a remote shell never looks like a local one, &lt;code&gt;ssh&lt;/code&gt; and &lt;code&gt;herdr --remote&lt;/code&gt; now tint the Ghostty background and set a &lt;code&gt;REMOTE: &amp;lt;host&amp;gt;&lt;/code&gt; title, and reset both when I leave. The core is small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;_remote_tint_run&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;label color
  &lt;span class="nv"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;_remote_tint_label &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;   &lt;span class="c"&gt;# me@home-ts.example.dev -&amp;gt; home&lt;/span&gt;
  &lt;span class="nb"&gt;shift&lt;/span&gt;
  &lt;span class="c"&gt;# Only for an interactive shell in Ghostty, with a tty on stdout.&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; interactive &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; 1 &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$TERM&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; xterm-ghostty &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="nv"&gt;$GHOSTTY_RESOURCES_DIR&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$?&lt;/span&gt;
  &lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nv"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REMOTE_TINT_COLORS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$label&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;$REMOTE_TINT_DEFAULT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;rc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
  &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'\e]11;%s\a\e]2;REMOTE: %s\a'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$color&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$label&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/tty
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;rc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt; always &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'\e]111\a'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/tty    &lt;span class="c"&gt;# back to the theme background&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$rc&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The escape codes go to &lt;code&gt;/dev/tty&lt;/code&gt;, so piped output stays clean. The &lt;code&gt;always&lt;/code&gt; block resets the color even when the command fails.&lt;/p&gt;

&lt;p&gt;The wrapper around &lt;code&gt;ssh&lt;/code&gt; is most of the file. It reads the arguments to find the host, and skips the tint when there is no interactive shell to tint: &lt;code&gt;ssh home uptime&lt;/code&gt;, &lt;code&gt;ssh -N&lt;/code&gt;, &lt;code&gt;ssh -T&lt;/code&gt;. &lt;code&gt;-p 22&lt;/code&gt; takes the next word and &lt;code&gt;-p22&lt;/code&gt; doesn't, so it has to know which options take a value.&lt;/p&gt;

&lt;p&gt;The color is deep purple. Inside herdr, it shows only where herdr doesn't paint its own panel colors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rules for a laptop that holds work credentials
&lt;/h2&gt;

&lt;p&gt;Phone control of agents is a new way into the work laptop. These are the rules I follow for it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Device lock on in the phone app.&lt;/li&gt;
&lt;li&gt;My own phone is the only controller.&lt;/li&gt;
&lt;li&gt;If the phone is lost: revoke it in the app, and run &lt;code&gt;stable-teardown&lt;/code&gt; on the work laptop.&lt;/li&gt;
&lt;li&gt;The work laptop accepts no incoming connections: Remote Login off, Shields Up on, and the Tailscale policy denies it.&lt;/li&gt;
&lt;li&gt;Tailscale on the work laptop is a personal network, signed in with a personal identity, as an outgoing client only. No exit node and no subnet routes on it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cert.pem&lt;/code&gt;, tunnel tokens, and pairing QRs are passwords. They never go into dotfiles, a chat, or a screenshot.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Versions
&lt;/h2&gt;

&lt;p&gt;What runs today:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;herdr&lt;/td&gt;
&lt;td&gt;0.9.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Herdr.app&lt;/td&gt;
&lt;td&gt;20261013.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;herdr-mobile-relay&lt;/td&gt;
&lt;td&gt;commit &lt;code&gt;6211568&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cloudflared&lt;/td&gt;
&lt;td&gt;2026.10.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tailscale&lt;/td&gt;
&lt;td&gt;1.102.4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The companion folder has the remote tint as a standalone zsh file, with tests, and the Tailscale policy from this post. The tests run an interactive zsh in a pseudo-terminal with fake &lt;code&gt;ssh&lt;/code&gt; and &lt;code&gt;herdr&lt;/code&gt;, so they need no Ghostty and no hosts. They check when the tint should happen and when it shouldn't, the host label, custom colors, and that the color resets when ssh fails.&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;cd &lt;/span&gt;03-mobile-relay
make &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/avivl/golem-notes-examples/tree/main/03-mobile-relay" rel="noopener noreferrer"&gt;golem-notes-examples/03-mobile-relay&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How do you reach your agents when you're not at the laptop they run on? Leave a comment.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is part 3 of my series on running a real agentic dev setup. I publish each part first on Golem Notes, with the full write-up and what's coming next. Subscribe there to get the next one in your inbox: &lt;a href="https://golemnotes.substack.com/p/my-agents-on-two-laptops-one-phone-app?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=agentic-setup-03" rel="noopener noreferrer"&gt;Golem Notes&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devtools</category>
      <category>productivity</category>
      <category>cloudflare</category>
    </item>
    <item>
      <title>Close the laptop, the agent keeps working</title>
      <dc:creator>Aviv Laufer</dc:creator>
      <pubDate>Thu, 15 Oct 2026 13:01:37 +0000</pubDate>
      <link>https://dev.to/avivl/close-the-laptop-the-agent-keeps-working-34pg</link>
      <guid>https://dev.to/avivl/close-the-laptop-the-agent-keeps-working-34pg</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Part 2 of My Agentic Setup. &lt;a href="https://dev.to/avivl/i-talk-to-one-agent-it-runs-the-others-26k4"&gt;Start from part 1&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;When an agent runs on my laptop, it stops when I close the laptop. That is fine for a five minute task. It is not fine for an audit that takes an hour, or a crew of agents working through a backlog.&lt;/p&gt;

&lt;p&gt;So my long agent work runs on a cloud VM, inside tmux. The laptop is only a window into it.&lt;/p&gt;

&lt;p&gt;The whole thing is one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agent-session.sh my-vm &lt;span class="nt"&gt;--gcloud&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm on the VM. I start &lt;code&gt;omp&lt;/code&gt;, give it work, and close the laptop. Tomorrow I run the same command, from the same laptop or the other one, and I'm back in the same place. The agent kept going while I was away.&lt;/p&gt;

&lt;p&gt;The idea is simple: the agent runs on the VM inside tmux, and the laptop is only a window, so one command starts the work or brings me back to it from any machine.&lt;/p&gt;

&lt;p&gt;Getting there was less simple. Here is what broke first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The VM was too small
&lt;/h3&gt;

&lt;p&gt;My first VM was a 1 GB e2-micro. omp got killed by the OOM killer while it was generating its shell completions. Not while running an agent, just while setting up the shell.&lt;/p&gt;

&lt;p&gt;Small doesn't mean slow here. It means broken. I moved to a larger VM, and now the troubleshooting table in my docs has this line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;a tool is killed with exit 137    out of memory; check free -h, resize the VM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The login never finished
&lt;/h3&gt;

&lt;p&gt;Every agent CLI logs in on each machine separately. My laptop's logins don't carry over to the VM, and I don't want them to.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;claude login&lt;/code&gt; opens a browser flow and then waits for the callback on &lt;code&gt;http://localhost:54545/callback&lt;/code&gt;. On the VM, that is the VM's localhost. The browser is on my laptop. It authorizes, redirects to port 54545 on the laptop, finds nothing there, and the login never completes. The browser just spins.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frv6umv92b6edyum9ag7n.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frv6umv92b6edyum9ag7n.png" alt="Why the login never finished: without --forward-auth the browser redirects to the laptop's localhost, where nothing listens. With --forward-auth an ssh tunnel carries the callback to the VM, where claude login is waiting." width="800" height="1481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fix is to forward the callback ports back over ssh. The script has a flag for it:&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="c"&gt;# Ports the agent CLIs listen on for their OAuth callback. Forwarded so the&lt;/span&gt;
&lt;span class="c"&gt;# browser on this laptop can reach a login running on the VM.&lt;/span&gt;
&lt;span class="nv"&gt;AUTH_PORTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"54545 54546 8976"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FORWARD_AUTH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 1 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  for &lt;/span&gt;port &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nv"&gt;$AUTH_PORTS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;forward_args+&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;port&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:localhost:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;port&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;done
fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the first time on a new VM I connect with &lt;code&gt;--forward-auth&lt;/code&gt;, then run &lt;code&gt;claude login&lt;/code&gt; and &lt;code&gt;cursor-agent login&lt;/code&gt; inside the session. When a tool has a device code flow, like &lt;code&gt;gh auth login --web&lt;/code&gt;, I use that instead. It needs no tunnel at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;Unknown scope: use&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Even with the tunnel, a login can fail with &lt;code&gt;Unknown scope: use&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The cause: the login URL was in a boxed TUI panel. The panel wraps the long URL, and copying the visible lines cuts off the end of the scope list. The browser gets a broken URL.&lt;/p&gt;

&lt;p&gt;The fix: copy the link from a plain, wide terminal. Or send the output to a file and take the URL from there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude login 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;tee&lt;/span&gt; /tmp/login.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Quoting in &lt;code&gt;--run&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;I wanted to start a job without attaching first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agent-session.sh my-vm &lt;span class="nt"&gt;--gcloud&lt;/span&gt; &lt;span class="nt"&gt;--run&lt;/span&gt; &lt;span class="s1"&gt;'omp "audit the auth module and report back"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That command goes through three layers: ssh, the shell on the VM, and tmux. Each one has its own quoting rules. A command with quotes inside quotes doesn't survive all three.&lt;/p&gt;

&lt;p&gt;The fix is to not quote it at all. The script base64-encodes the command on the laptop and decodes it on the VM:&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="c"&gt;# The command crosses ssh, the remote shell and tmux, each with its own&lt;/span&gt;
&lt;span class="c"&gt;# quoting. base64 is the one encoding that survives all three unchanged.&lt;/span&gt;
&lt;span class="nv"&gt;encoded&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'\n'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;remote&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"tmux new-session -A -d -s &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;session_q&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
        tmux new-window -t &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;session_q&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;sh -lc 'eval &lt;/span&gt;&lt;span class="se"&gt;\\\"\\\$&lt;/span&gt;&lt;span class="s2"&gt;(printf %s &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;encoded&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; | base64 -d)&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;; exec &lt;/span&gt;&lt;span class="se"&gt;\\\$&lt;/span&gt;&lt;span class="s2"&gt;SHELL -l'&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
        tmux attach -t &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;session_q&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is not pretty, but base64 has only letters, digits, &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;/&lt;/code&gt; and &lt;code&gt;=&lt;/code&gt;, so none of the three layers has anything to escape.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;exec $SHELL -l&lt;/code&gt; at the end matters too. When the agent finishes, the window stays open with a shell in it, so the final answer is still on screen when I come back.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running the script on the VM itself
&lt;/h3&gt;

&lt;p&gt;The script is meant to run on the laptop. Run it on the VM by mistake, and &lt;code&gt;gcloud compute ssh&lt;/code&gt; tries to ssh from the VM to itself. It generates a new SSH key there and fails on permissions the VM's own service account doesn't have.&lt;/p&gt;

&lt;p&gt;The fix is a hostname check. If I'm already on the target machine, the script skips ssh and just attaches:&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="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;hostname&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOST&lt;/span&gt;&lt;span class="p"&gt;%%.*&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;exec &lt;/span&gt;tmux new-session &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SESSION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the same command does the right thing from both places.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;command not found: omp&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This one happens on a fresh VM. You connect, tmux starts, then you install the tools. Inside the session, &lt;code&gt;omp&lt;/code&gt; is not found.&lt;/p&gt;

&lt;p&gt;The tmux server started before the tools were installed, so it still has the old PATH, and every new window gets that old PATH too. Reconnecting doesn't help, because the server is still running.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tmux kill-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then connect again.&lt;/p&gt;

&lt;h3&gt;
  
  
  Smaller ones
&lt;/h3&gt;

&lt;p&gt;Some issues came from setting up the VM, not from running agents on it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A minimal Debian image has no &lt;code&gt;make&lt;/code&gt;, and my setup runs &lt;code&gt;make&lt;/code&gt;. It is in the first install step now.&lt;/li&gt;
&lt;li&gt;Debian ships Node 18. Some of my npm tools need 22. Linux takes Node from mise instead of apt.&lt;/li&gt;
&lt;li&gt;The ssh client forwards a locale the VM didn't generate, which prints a &lt;code&gt;setlocale&lt;/code&gt; warning on every login. It is harmless. Installing the locale on the VM, or not forwarding it, makes it go away.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The VM comes from the same dotfiles
&lt;/h3&gt;

&lt;p&gt;The VM is not special. It gets the same dotfiles as my laptops, with a Linux layer on top. This is the whole OS profile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# profiles/os/linux.toml&lt;/span&gt;
&lt;span class="c"&gt;# Linux defaults, targeting headless Debian and Ubuntu LTS hosts.&lt;/span&gt;

&lt;span class="py"&gt;capabilities&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"cli"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"linux"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[shell]&lt;/span&gt;
&lt;span class="py"&gt;ssh_use_keychain&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="nn"&gt;[packages]&lt;/span&gt;
&lt;span class="py"&gt;packs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"base"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"agents"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"formal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"linux"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;cli&lt;/code&gt; without &lt;code&gt;desktop&lt;/code&gt; keeps desktop applications off a headless machine. &lt;code&gt;ssh_use_keychain = false&lt;/code&gt; matters because &lt;code&gt;UseKeychain&lt;/code&gt; is a macOS-only SSH option, and on Linux it makes ssh reject the whole config file.&lt;/p&gt;

&lt;h3&gt;
  
  
  The agents pack
&lt;/h3&gt;

&lt;p&gt;The agent tools come from a package pack. On macOS most of them are Homebrew formulas. On Linux most don't have an apt package, so each row has an upstream installer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[[package]]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"can1357/tap/omp"&lt;/span&gt;
&lt;span class="py"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;macos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"brew"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;linux&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"upstream"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;binary&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"omp"&lt;/span&gt;
&lt;span class="py"&gt;install&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;linux&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"...curl the release binary into ~/.local/bin..."&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nn"&gt;[[package]]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"claude-code"&lt;/span&gt;
&lt;span class="py"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;macos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"upstream"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;linux&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"upstream"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;install&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"curl -fsSL https://claude.ai/install.sh | bash"&lt;/span&gt;
&lt;span class="py"&gt;binary&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"claude"&lt;/span&gt;

&lt;span class="nn"&gt;[[package]]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"node"&lt;/span&gt;
&lt;span class="py"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;macos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"brew"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;linux&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"upstream"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;min_major&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;
&lt;span class="py"&gt;install&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;linux&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"mise use -g node@lts"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bootstrap script shows each upstream command and asks before running it. It also exits non-zero until everything is installed, so a half-installed VM doesn't look finished.&lt;/p&gt;

&lt;p&gt;What runs on the VM today:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it does there&lt;/th&gt;
&lt;th&gt;Installed on Linux by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;tmux&lt;/td&gt;
&lt;td&gt;keeps sessions alive&lt;/td&gt;
&lt;td&gt;apt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;omp&lt;/td&gt;
&lt;td&gt;my main agent&lt;/td&gt;
&lt;td&gt;release binary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;crew agent&lt;/td&gt;
&lt;td&gt;its install script&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursor agent&lt;/td&gt;
&lt;td&gt;crew agent&lt;/td&gt;
&lt;td&gt;its install script&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firstmate&lt;/td&gt;
&lt;td&gt;runs the crew&lt;/td&gt;
&lt;td&gt;git clone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;worktrunk&lt;/td&gt;
&lt;td&gt;worktrees&lt;/td&gt;
&lt;td&gt;release tarball&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node&lt;/td&gt;
&lt;td&gt;for the npm tools&lt;/td&gt;
&lt;td&gt;mise&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  New machine, in order
&lt;/h3&gt;

&lt;p&gt;Setting up a VM follows the same eight steps as a laptop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Base tools: git, ssh, make, Python, chezmoi&lt;/li&gt;
&lt;li&gt;A new SSH key for this machine, added to GitHub&lt;/li&gt;
&lt;li&gt;Clone the dotfiles&lt;/li&gt;
&lt;li&gt;A dry run into a fake home&lt;/li&gt;
&lt;li&gt;Declare the machine in a profile&lt;/li&gt;
&lt;li&gt;Install the software&lt;/li&gt;
&lt;li&gt;Install the configuration&lt;/li&gt;
&lt;li&gt;Log in to each tool&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Configuration and software never mix. Step 7 installs no packages, and step 6 changes no configuration. Logins are always last, and always per machine. Nothing is copied from another machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the script works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  One tmux command does most of the work
&lt;/h3&gt;

&lt;p&gt;The whole idea is one tmux flag:&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="nv"&gt;remote&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"tmux new-session -A -s &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; %q &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SESSION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;exec &lt;/span&gt;ssh &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$remote&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;new-session -A&lt;/code&gt; attaches to the session if it exists and creates it if it doesn't. So the first connection and every connection after it are the same command.&lt;/p&gt;

&lt;p&gt;ssh is only the transport. &lt;code&gt;-t&lt;/code&gt; gives it a terminal, because tmux refuses to attach without one.&lt;/p&gt;

&lt;p&gt;On GCP the script uses &lt;code&gt;gcloud compute ssh&lt;/code&gt; with &lt;code&gt;--tunnel-through-iap&lt;/code&gt;. My VM has no external IP, so IAP is how I reach it.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fikayok6sixdyfwqdnoih.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fikayok6sixdyfwqdnoih.png" alt="One session, any laptop: the work laptop and the personal laptop both connect over ssh with IAP to my-vm, which is always on. Its tmux server has a session called agents with omp in window 1 and fm payments in window 2, and a session called review with claude." width="800" height="1259"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Less typing
&lt;/h3&gt;

&lt;p&gt;The machine, project and zone are the same every time, so I set them once:&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="c"&gt;# ~/.zshrc.local&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AGENT_SESSION_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-vm
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AGENT_SESSION_PROJECT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-project
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AGENT_SESSION_ZONE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-zone
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now it is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agent-session.sh &lt;span class="nt"&gt;--gcloud&lt;/span&gt;            &lt;span class="c"&gt;# connect&lt;/span&gt;
agent-session.sh &lt;span class="nt"&gt;--gcloud&lt;/span&gt; &lt;span class="nt"&gt;--list&lt;/span&gt;     &lt;span class="c"&gt;# what is running&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--list&lt;/code&gt; is my dashboard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;billing: 1 windows (created Mon Sep 14 09:57:17 2026)
payments: 1 windows (created Mon Sep 14 09:56:16 2026)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Sessions, windows and tabs
&lt;/h3&gt;

&lt;p&gt;These three are easy to mix up, so here is how I use each one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A tmux window&lt;/strong&gt; (&lt;code&gt;Ctrl-b c&lt;/code&gt;, or &lt;code&gt;fm &amp;lt;project&amp;gt;&lt;/code&gt;): jobs I want to watch together. &lt;code&gt;Ctrl-b w&lt;/code&gt; picks one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A tmux session&lt;/strong&gt; (&lt;code&gt;--session &amp;lt;name&amp;gt;&lt;/code&gt;): jobs I want kept apart, each one reattachable on its own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Ghostty tab&lt;/strong&gt;: only a way to see two sessions at the same time. Closing it changes nothing on the VM.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One thing surprises people. Two Ghostty tabs attached to the same session show the same screen, live, with both cursors. That is nice for watching a long run on a second monitor, and useless for doing two things at once. Different work needs &lt;code&gt;--session&lt;/code&gt;, not another tab.&lt;/p&gt;

&lt;h3&gt;
  
  
  What survives
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Closing the laptop, losing wifi, disconnecting: keeps running.&lt;/li&gt;
&lt;li&gt;Connecting from the other laptop: same sessions, same screens.&lt;/li&gt;
&lt;li&gt;Two laptops connected at once: both see the same screen.&lt;/li&gt;
&lt;li&gt;Rebooting the VM: the agents stop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The VM is always on, so the last one almost never happens. I have tmux-resurrect and tmux-continuum installed, so if the VM does reboot, the sessions and windows come back. An agent that was in the middle of its work doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I am now
&lt;/h2&gt;

&lt;p&gt;These days I mostly use herdr for this. It does the same job as tmux on the VM, with its own way of saving machines and reconnecting. Use one or the other, not both: herdr inside tmux makes their keyboard shortcuts fight.&lt;/p&gt;

&lt;p&gt;The pattern is the same either way. The agent runs on a machine that stays on, and the laptop is only a window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The companion repo has a sanitized copy of &lt;code&gt;agent-session.sh&lt;/code&gt; and a test suite. The tests run tmux on a private socket with fake &lt;code&gt;ssh&lt;/code&gt; and &lt;code&gt;gcloud&lt;/code&gt;, so they don't need a VM and don't touch your own tmux server. They check the &lt;code&gt;--run&lt;/code&gt; quoting with quotes, &lt;code&gt;$&lt;/code&gt; and backticks, the local attach, &lt;code&gt;--forward-auth&lt;/code&gt; and the IAP flags.&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;cd &lt;/span&gt;02-remote-agents
make &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/avivl/golem-notes-examples/tree/main/02-remote-agents" rel="noopener noreferrer"&gt;golem-notes-examples/02-remote-agents&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Where do your long agent runs live: your laptop, a VM, something else? Leave a comment.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is part 2 of my series on running a real agentic dev setup. I publish each part first on Golem Notes, with the full write-up and what's coming next. Subscribe there to get the next one in your inbox: &lt;a href="https://golemnotes.substack.com/p/close-the-laptop-the-agent-keeps?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=agentic-setup-02" rel="noopener noreferrer"&gt;Golem Notes&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devtools</category>
      <category>productivity</category>
      <category>tmux</category>
    </item>
    <item>
      <title>I talk to one agent. It runs the others.</title>
      <dc:creator>Aviv Laufer</dc:creator>
      <pubDate>Mon, 05 Oct 2026 12:49:31 +0000</pubDate>
      <link>https://dev.to/avivl/i-talk-to-one-agent-it-runs-the-others-26k4</link>
      <guid>https://dev.to/avivl/i-talk-to-one-agent-it-runs-the-others-26k4</guid>
      <description>&lt;p&gt;&lt;em&gt;The map of my setup: OMP in the middle, the tools it dispatches, and the 13 parts coming next.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most of my coding day happens in one terminal pane. I type into OMP, and OMP does the rest.&lt;/p&gt;

&lt;p&gt;It picks a model for each job. Planning goes to Opus. Normal turns go to Sonnet. Reviews go to a GPT model, so the reviewer is never the same model that wrote the code. When the work is big, OMP hands it to a fleet: Firstmate spawns crew agents, each one in its own worktree, and most of the time the crew is working and I'm not.&lt;/p&gt;

&lt;p&gt;I use several coding agents, but I don't configure them separately. One repo decides which rules, skills, identities and models every agent gets, on every machine.&lt;/p&gt;

&lt;p&gt;That repo is my dotfiles. It started in December 2016 with a tmux and vim config. In the last three months, since agents took over my workflow, it got almost as many commits as in the nine years before.&lt;/p&gt;

&lt;p&gt;This post is the map: every tool, how they connect, what broke when I first wired them together, and the fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  The map
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                         me
                          |
                          v
   +---------------------------------------------+
   |  OMP  (my main tool, one model per role)    |
   +---------------------------------------------+
            |                        |
            | talks to models        | fm &amp;lt;project&amp;gt;
            | directly               v
            |              +----------------------+
            |              |  Firstmate           |
            |              |  (first mate on OMP) |
            |              +----------------------+
            |                 |   dispatch rules
            v                 v
   Anthropic, Cursor,     crew: OMP, Claude Code,
   OpenAI, local model    Codex, Grok
                              |
                              v
                     treehouse worktrees
                     (one per task, pooled)

   herdr on the laptop, tmux on the VM: every agent gets a pane
   dotfiles: renders the config for all of the above
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It runs on two laptops and a cloud VM. My work laptop gets the company rules: our skills, our ticket conventions, our compliance rules. My personal laptop should not see any of that, and the two use different model accounts.&lt;/p&gt;

&lt;p&gt;I'll go through each box below. First, the part that went wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke
&lt;/h2&gt;

&lt;p&gt;OMP is where I type, but it is not the only agent that runs. Firstmate starts Claude Code, Codex or Grok as crew, and Cursor agent is there too. Each of these tools reads its instructions from a different place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.claude/CLAUDE.md
~/.codex/AGENTS.md
~/.pi/agent/AGENTS.md
~/.agents/AGENTS.md
~/.gemini/GEMINI.md
~/.config/opencode/AGENTS.md
~/.copilot/copilot-instructions.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven files that should say the same thing. Skills have the same problem: each tool has its own skills folder.&lt;/p&gt;

&lt;p&gt;Agents read their instructions in every session, so when those instructions drift, the agents drift with them. Nothing warns you.&lt;/p&gt;

&lt;p&gt;Two things were wrong.&lt;/p&gt;

&lt;p&gt;First, the files had drifted. My &lt;code&gt;~/.codex/AGENTS.md&lt;/code&gt; was a copy I'd made by hand in May, and it was out of date. Pi, another agent I had installed, saw only 3 of my 23 skills. Each tool was working with a different set of rules, and I had assumed they were all the same.&lt;/p&gt;

&lt;p&gt;Second, the instructions were too long. The global file had grown to 465 lines, about 21 KB or 5,300 tokens. It loads on every turn, in Claude Code and each of its subagents, in OMP, Codex, Pi, Gemini, OpenCode and Copilot. So every one of those paid for all 465 lines, every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;h3&gt;
  
  
  One source, compiled per machine
&lt;/h3&gt;

&lt;p&gt;I split the instructions into small fragments, one topic per file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;agents/fragments/
  00-header.md
  05-no-emojis.md
  10-voice.md
  20-workflow.md
  30-git-conventions.md
  32-worktrees-stacking.md
  35-pull-requests.md
  45-core-principles.md
  ...
  50-work-skills.md     # work only
  55-compliance.md      # work only
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each machine profile is a plain list of the fragments it gets. The personal profile leaves out the work-only ones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# agents/machines/personal.txt
00-header.md
05-no-emojis.md
20-workflow.md
35-pull-requests.md
32-worktrees-stacking.md
45-core-principles.md
30-git-conventions.md
40-languages.md
10-voice.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;make build&lt;/code&gt; puts them together into one flat &lt;code&gt;AGENTS.md&lt;/code&gt;, with a marker before each fragment so I can see where a rule came from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="err"&gt;while&lt;/span&gt; &lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; name&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="err"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"$name"&lt;/span&gt; &lt;span class="err"&gt;in&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s1"&gt;'#'&lt;/span&gt;&lt;span class="err"&gt;*)&lt;/span&gt; &lt;span class="err"&gt;continue&lt;/span&gt; &lt;span class="err"&gt;;;&lt;/span&gt; &lt;span class="err"&gt;esac&lt;/span&gt;
  &lt;span class="nl"&gt;printf '&amp;lt;!-- Source&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fragments/%s --&amp;gt;&lt;/span&gt;\n&lt;span class="nf"&gt;' "$name"&lt;/span&gt;
  &lt;span class="err"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"fragments/$name"&lt;/span&gt;
&lt;span class="err"&gt;done&lt;/span&gt; &lt;span class="err"&gt;&amp;lt;&lt;/span&gt; &lt;span class="err"&gt;machines/$(PROFILE).txt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why flat and not includes? Codex, Pi and Cursor don't expand any include syntax. So the modular part lives in the source, and every tool gets one plain file.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;make deploy&lt;/code&gt; then symlinks that one file into all seven paths. There are no copies left to go stale. When I edit a fragment and run &lt;code&gt;make build&lt;/code&gt;, every tool picks it up in its next session.&lt;/p&gt;

&lt;h3&gt;
  
  
  A check that blocks drift
&lt;/h3&gt;

&lt;p&gt;A pre-commit hook runs &lt;code&gt;make check&lt;/code&gt;. It builds again into a temp folder and diffs the result against the built file. If I edit a fragment and forget to build, the commit fails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;check: build/personal/AGENTS.md is out of date. Run: make -C agents build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  One skills folder
&lt;/h3&gt;

&lt;p&gt;Skills go in one place, &lt;code&gt;~/.agents/skills/&lt;/code&gt;. When I checked in July, Codex, Cursor, OpenCode, Gemini and Copilot all read that folder directly. Claude Code doesn't, so &lt;code&gt;make skills-sync&lt;/code&gt; adds a symlink per skill into &lt;code&gt;~/.claude/skills/&lt;/code&gt;. It does the same for Pi's own folder, which is where the 3-of-23 problem was.&lt;/p&gt;

&lt;p&gt;It never overwrites a real file. A skill installed some other way gets skipped with a warning, not deleted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then I could cut it in half
&lt;/h2&gt;

&lt;p&gt;With one file, I could finally see everything in one place. So I cut it. 465 lines down to 215. About 5,300 tokens down to 2,500.&lt;/p&gt;

&lt;p&gt;The biggest cut was the pull request procedure. I moved it, as is, into a skill. It loads only when an agent works on a PR. The fragment kept only the hard rules: draft only, no merge and no deploy without my approval. The rest of the fragments I trimmed down to the rules, without the explanations.&lt;/p&gt;

&lt;p&gt;With seven copies, I never cut anything. I didn't know which copy was the real one, so I only added. With one source, cutting is safe.&lt;/p&gt;

&lt;p&gt;Now I split instructions into two kinds. Rules every agent needs on every turn go in the global file, and I keep it short. Procedures for one kind of task go in a skill. An agent renaming a variable doesn't need my PR procedure, and it shouldn't pay for it in tokens.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rest of the map
&lt;/h2&gt;

&lt;p&gt;With that in place, here is what each box in the diagram does.&lt;/p&gt;

&lt;h3&gt;
  
  
  OMP
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/can1357/oh-my-pi" rel="noopener noreferrer"&gt;OMP&lt;/a&gt; (Oh My Pi) is a coding agent that talks to the model APIs directly. I log in to Anthropic, Cursor and OpenAI once, and OMP routes each kind of work to a model through roles:&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;modelRoles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cursor/claude-sonnet-5-5:medium&lt;/span&gt;
  &lt;span class="na"&gt;plan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;cursor/claude-opus-5-5:medium&lt;/span&gt;
  &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;cursor/claude-sonnet-5-5:medium&lt;/span&gt;
  &lt;span class="na"&gt;slow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;cursor/composer-2.5:high&lt;/span&gt;
  &lt;span class="na"&gt;smol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;cursor/gpt-5.6-luna:medium&lt;/span&gt;
  &lt;span class="na"&gt;commit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;cursor/gpt-5.6-luna:low&lt;/span&gt;
  &lt;span class="na"&gt;tiny&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;local/lfm2.5-230m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reviewer is set in a separate block of the same config file:&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;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;agentModelOverrides&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;reviewer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cursor/gpt-5.6-terra:high&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's my personal laptop. It only has Claude and Cursor accounts, so almost everything goes through Cursor. The work laptop uses the same roles with different providers, and dotfiles picks the right ones per machine.&lt;/p&gt;

&lt;p&gt;Model names change every few months, so here is the current table. I'll keep it up to date. The pattern is what matters.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Current model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;default&lt;/td&gt;
&lt;td&gt;normal turns&lt;/td&gt;
&lt;td&gt;Claude Sonnet 5.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;plan&lt;/td&gt;
&lt;td&gt;planning&lt;/td&gt;
&lt;td&gt;Claude Opus 5.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;task&lt;/td&gt;
&lt;td&gt;subagent work&lt;/td&gt;
&lt;td&gt;Claude Sonnet 5.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;slow&lt;/td&gt;
&lt;td&gt;hard fixes&lt;/td&gt;
&lt;td&gt;Composer 2.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reviewer&lt;/td&gt;
&lt;td&gt;code review, different vendor than the author&lt;/td&gt;
&lt;td&gt;GPT-5.6 Terra&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;smol&lt;/td&gt;
&lt;td&gt;cheap fan-out&lt;/td&gt;
&lt;td&gt;GPT-5.6 Luna&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;commit&lt;/td&gt;
&lt;td&gt;commit messages&lt;/td&gt;
&lt;td&gt;GPT-5.6 Luna, low effort&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tiny&lt;/td&gt;
&lt;td&gt;throwaway calls&lt;/td&gt;
&lt;td&gt;a 230M local model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;judge&lt;/td&gt;
&lt;td&gt;"is this actually done?"&lt;/td&gt;
&lt;td&gt;a small model on Cloudflare Workers AI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Firstmate
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/kunchenguid/firstmate" rel="noopener noreferrer"&gt;Firstmate&lt;/a&gt; runs a fleet of agents. I'm the captain. The first mate is an OMP session that takes my ask, writes a brief and starts crew agents to do the work. &lt;code&gt;fm &amp;lt;project&amp;gt;&lt;/code&gt; opens it for one project, and each project gets its own home, so backlogs and running tasks never mix.&lt;/p&gt;

&lt;p&gt;The crew is OMP by default, but a few dispatch rules send work to other tools:&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;"when"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The task is a trivial mechanical edit: rote rename, formatting sweep, targeted typo fix, dependency bump, or gathering files."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"use"&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;"harness"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude"&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="s2"&gt;"haiku"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"effort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"low"&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="err"&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;"when"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The task is a scout: investigate, audit, trace a bug, or produce a standalone report without changing code."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"use"&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;"harness"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"omp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"effort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&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;My preferences for the first mate live in a markdown file it reads at the start of every session. A few lines from it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;-&lt;/span&gt; Never use em dashes in PR titles, PR bodies, commit messages, or comments.
&lt;span class="p"&gt;-&lt;/span&gt; Every PR any worker opens is created in draft mode, no exceptions.
&lt;span class="p"&gt;-&lt;/span&gt; Escalate to me on any change touching auth, tenancy boundaries, data
  retention, or migration ordering, even when the tests are green.
&lt;span class="p"&gt;-&lt;/span&gt; Scout tasks end in a report, never a code change.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Worktrees: two tools on purpose
&lt;/h3&gt;

&lt;p&gt;When I start an agent myself, I use &lt;a href="https://github.com/max-sixty/worktrunk" rel="noopener noreferrer"&gt;worktrunk&lt;/a&gt; (&lt;code&gt;wt&lt;/code&gt;). One alias creates a branch, a worktree and an agent session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;worktree-path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"~/worktrees/{{ repo }}/{{ branch | sanitize }}"&lt;/span&gt;

&lt;span class="nn"&gt;[aliases]&lt;/span&gt;
&lt;span class="py"&gt;cc&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"wt switch --create {{ branch }} -x claude"&lt;/span&gt;
&lt;span class="py"&gt;cur&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"wt switch --create {{ branch }} -x wt-agent -- cursor-agent"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When Firstmate starts a crew agent, it uses treehouse. Treehouse keeps a pool of worktrees per repo and leases one out per task (&lt;code&gt;treehouse get --lease&lt;/code&gt;, then &lt;code&gt;treehouse return&lt;/code&gt;). Two crew agents on the same repo get slot 1 and slot 2 and never collide. Most of the worktrees on my machine come from treehouse, because most of the time the crew is the one working.&lt;/p&gt;

&lt;h3&gt;
  
  
  herdr and tmux
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://herdr.dev" rel="noopener noreferrer"&gt;herdr&lt;/a&gt; is the terminal multiplexer on my laptops. Unlike tmux, it knows which pane is an agent and whether that agent is working, blocked, done or idle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[ui]&lt;/span&gt;
&lt;span class="py"&gt;agent_panel_sort&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"spaces"&lt;/span&gt;     &lt;span class="c"&gt;# one space per repo/worktree&lt;/span&gt;
&lt;span class="py"&gt;status_indicators&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"symbols"&lt;/span&gt;   &lt;span class="c"&gt;# blocked/working/done/idle&lt;/span&gt;
&lt;span class="py"&gt;show_agent_labels_on_pane_borders&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With several crew agents running, &lt;code&gt;ctrl+alt+up&lt;/code&gt; and &lt;code&gt;ctrl+alt+down&lt;/code&gt; jump between them. On the cloud VM I use plain tmux, so agents keep running after I close the laptop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dotfiles
&lt;/h3&gt;

&lt;p&gt;Everything above is rendered from the dotfiles repo, in layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;base &amp;lt; os &amp;lt; organization &amp;lt; machine &amp;lt; local.toml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each layer only overrides the values it names. The work laptop declares the work organization and the personal one. The personal laptop declares only personal. A repo outside every declared root has no git identity at all, so a commit fails instead of going out under the wrong name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The companion repo has a small working version of the fragments build: four fragments (one of them work-only), two profiles, a pre-commit hook, and the Makefile with &lt;code&gt;build&lt;/code&gt;, &lt;code&gt;check&lt;/code&gt; and &lt;code&gt;deploy&lt;/code&gt;. By default it deploys into a fake home folder, so it never touches your real config. When you point it at your real home, it skips any real file in the way instead of overwriting it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; make deploy
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; fake-home/.codex   &lt;span class="c"&gt;# AGENTS.md -&amp;gt; .../build/personal/AGENTS.md&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/avivl/golem-notes-examples/tree/main/01-the-map" rel="noopener noreferrer"&gt;golem-notes-examples/01-the-map&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's coming
&lt;/h2&gt;

&lt;p&gt;One part every two weeks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The map&lt;/strong&gt; (this post)&lt;/li&gt;
&lt;li&gt;Close the laptop, the agent keeps working: agents on a cloud VM with tmux&lt;/li&gt;
&lt;li&gt;My agents on two laptops, controlled from one phone app&lt;/li&gt;
&lt;li&gt;Every PR gets an agent review before a human sees it&lt;/li&gt;
&lt;li&gt;Captain, first mate, crew: orchestrating a fleet of agents&lt;/li&gt;
&lt;li&gt;Nine roles, not one model&lt;/li&gt;
&lt;li&gt;Right model for the task: dispatch rules and cheaper subagents&lt;/li&gt;
&lt;li&gt;When the credits run out: fallback chains&lt;/li&gt;
&lt;li&gt;A cheap judge for "is this actually done?"&lt;/li&gt;
&lt;li&gt;Teaching agents how I work: my skills library&lt;/li&gt;
&lt;li&gt;Dotfiles an agent can't break&lt;/li&gt;
&lt;li&gt;One laptop, many companies: identities and secrets&lt;/li&gt;
&lt;li&gt;Testing your dotfiles like production code&lt;/li&gt;
&lt;li&gt;What I'd do differently&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you run more than one agent tool, I'd like to know how you keep their rules in sync. Leave a comment.&lt;/p&gt;




&lt;p&gt;This is part 1 of my series on running a real agentic dev setup. I publish each part first on Golem Notes, with the full write-up and what's coming next. Subscribe there to get the next one in your inbox: &lt;a href="https://golemnotes.substack.com/p/i-talk-to-one-agent-it-runs-the-others?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=agentic-setup-01" rel="noopener noreferrer"&gt;https://golemnotes.substack.com/p/i-talk-to-one-agent-it-runs-the-others?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=agentic-setup-01&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>devtools</category>
      <category>dotfiles</category>
    </item>
  </channel>
</rss>
