Agent patterns¶
Agents in a VM shows the basics: Claude Code inside a locked VM, in tmux, headless and in throwaway VMs. This page builds on it with patterns for running several agents, fitting each one's sandbox to its job, having agents check each other's work, and running them while you're away. Each section says when to use it, gives the commands, and ends with what to watch.
The examples use a project whose VM is app and whose repository is
me/app, cloned in each VM to ~/code/github.com/me/app.
Several agents in one VM, with worktrees¶
When: a few independent tasks on the same repository, without the memory of a VM each.
git worktree gives each task its own checkout of the same repository,
on its own branch, so agents don't edit each other's files. Put the
worktrees under ~/code, which survives rebuilds:
limabean ssh # a shell in the clone
git worktree add ~/code/worktrees/app-docs -b agent/docs
git worktree add ~/code/worktrees/app-perf -b agent/perf
tmux new -d -s docs -c ~/code/worktrees/app-docs 'claude -p --dangerously-skip-permissions "update the README for the new CLI flags; commit" 2>&1 | tee ~/docs.log'
tmux new -d -s perf -c ~/code/worktrees/app-perf 'claude -p --dangerously-skip-permissions "find and fix the slowest test; commit" 2>&1 | tee ~/perf.log'
tmux ls lists the agents still running. Every worktree shares one
repository, so a single limabean remote
and git fetch limabean on the Mac bring back all their branches.
Watch: they share the VM's CPU, memory and network, so builds slow each
other down. limabean rm checks for unpushed commits on every branch,
worktrees' included, but not for uncommitted changes inside a worktree:
have the agents commit, or remove worktrees with git worktree remove,
before deleting the VM.
The same task against different versions¶
When: checking a change against several versions of a tool, such as two Go releases, or letting attempts differ in one way you choose.
Give each VM its own project file, naming the VM and pinning the tool that differs:
# matrix/go125.yaml
name: app-go125
repo: me/app
packs: [go, claude]
tools:
go: "1.25.4"
network:
allow: [github, go, claude]
locked: true
Then start the same task in each, as in attempts side by side, and fetch the branches with a remote per VM.
Watch: each VM installs its own project tools, so these VMs take longer to create than ones that only use packs.
Learn what an agent needs, then lock it¶
When: a new project, where you don't yet know which hosts its build and tests reach.
Let the first run happen in learning mode, which keeps the network open but records every name the VM looks up. Then turn what was used into the allowlist and lock it:
limabean network learn app
limabean ssh -- 'claude -p --dangerously-skip-permissions "install the dependencies and get the test suite passing"'
limabean network log app # every name it looked up, and how often
limabean network learn app --done # allow just those from now on
limabean network lock app
Later runs can reach only what the first one needed. Put the result in
the project file's network: so new VMs start locked
(Sandboxes).
Watch: during learning the agent can reach anything. Do it with a task
you'd trust on an open network, and read the list --done prints before
locking: a lookup made once by something unexpected is allowed too.
A secret for each role¶
When: agents with different jobs, such as one that pushes and one that only reads.
Shared secrets go to every VM, and so does the token from
limabean github login. Keep the shared file to what every VM may have,
and give tokens to the VMs that need them with --vm:
limabean secrets set GITHUB_TOKEN keychain:gh-app-writer --vm app # a fine-grained token that can push to me/app
limabean secrets set CLAUDE_CODE_OAUTH_TOKEN keychain:claude --vm app
limabean secrets set CLAUDE_CODE_OAUTH_TOKEN keychain:claude --vm reviewer
limabean secrets ls # which VM gets what
A reviewer then has a Claude login and nothing that can push, and a VM for code you don't trust gets nothing at all.
Watch: an agent can read every secret in its VM. A fine-grained GitHub token limited to one repository limits what a leaked one can do.
A writer and a reviewer¶
When: you want a second opinion before reading the change yourself, from an agent that can't change anything.
One VM writes a branch; a second, locked VM with no push rights reviews it. Your Mac carries the branch between them, so neither VM needs to reach the other, or GitHub, for it:
# A reviewer: Claude and the supply-chain scanners, no push token
limabean up reviewer --with claude,supply-chain --allow github --allow claude --locked
limabean get reviewer me/app
# In the checkout on the Mac: the writer's branch, from one VM to the other
limabean remote # the writer, as "limabean"
limabean remote reviewer --name reviewer # the reviewer's clone
git fetch limabean
git push reviewer limabean/agent/fix:refs/heads/agent/fix
limabean ssh reviewer -- 'cd ~/code/github.com/me/app && git diff main...agent/fix | claude -p "Review this change. List bugs, risks and missing tests, most serious first. Change nothing."'
The reviewer reads the diff from its input, so it needs no permission to
run commands. The review recipe is a project file for the
same kind of VM, with Claude's code-review plugin turned on.
Watch: the review is advice. It catches things, but it isn't a reason to skip reading the change yourself.
Checkpoints for a long task¶
When: a task with phases, where a failure late on shouldn't cost the early work.
Save a snapshot between phases, and roll back to the last good one rather than starting again:
limabean snapshot save app after-setup
# … the agent refactors …
limabean snapshot save app after-refactor
# … the migration goes wrong …
limabean snapshot restore app after-refactor
limabean snapshot ls app
A snapshot holds the VM's system and ~/code, uncommitted work included
(Snapshots).
Watch: saving shuts the VM down for a few seconds, which ends a running agent and its tmux session. Save between runs, not during one.
Claude on the Mac as the manager¶
When: you want one conversation to run several agents and report back, rather than juggling terminals.
Claude Code on your Mac can do the steps of
attempts side by side itself, over
MCP: clone_vm to make the copies, run_in_vm to start a tmux
agent in each and later to read their logs, and vm_info to see each
VM's clone and network. Ask it for the whole job:
Clone app twice. In each copy, start Claude in tmux on the task in ~/task.md. Check their logs every few minutes, and when both are done, compare their branches and tell me which to keep.
Watch: Claude Code asks before each tool you haven't allowed, and
run_in_vm can run anything in a VM, so read what it starts. It can't
widen a VM's network or delete one; you do that.
Watch the app while the agent works¶
When: front-end or API work, where seeing the result beats reading the diff.
A project file's start runs the dev server in the
background, and its port reaches your Mac's localhost:
As the agent edits files in the clone, the dev server reloads and you see
each change. limabean logs --start -f follows the server's output.
Watch: start runs commands from the project file on every up, so
limabean asks you to approve them the first time and whenever they change.
Agents on a schedule¶
When: chores that suit a quiet hour, such as updating dependencies overnight and opening a pull request.
A launchd agent on your Mac can run limabean run
on a schedule. Save this as
~/Library/LaunchAgents/com.example.limabean-deps.plist, with your
repository and task:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.example.limabean-deps</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/limabean</string>
<string>run</string><string>--rm</string><string>--plain</string>
<string>--with</string><string>claude</string>
<string>--allow</string><string>github</string>
<string>--allow</string><string>npm</string>
<string>--allow</string><string>claude</string>
<string>--locked</string>
<string>--repo</string><string>me/app</string>
<string>--</string>
<string>claude -p --dangerously-skip-permissions "update the npm dependencies within their major versions, run the tests, and if they pass push a branch and open a pull request with gh" </string>
</array>
<key>EnvironmentVariables</key>
<dict><key>PATH</key><string>/opt/homebrew/bin:/usr/bin:/bin</string></dict>
<key>StartCalendarInterval</key>
<dict><key>Hour</key><integer>3</integer><key>Minute</key><integer>17</integer></dict>
<key>StandardOutPath</key><string>/tmp/limabean-deps.log</string>
<key>StandardErrorPath</key><string>/tmp/limabean-deps.log</string>
</dict>
</plist>
The VM gets your shared secrets. To keep the Claude login and the GitHub
token out of the shared file, pass them to this VM alone with run's
--secret NAME=reference (for example a keychain: or op://
reference).
Watch: launchd runs it only while you're logged in and the Mac is awake. The pull request is the place to review the agent's work; don't let a scheduled agent merge.
Run code you don't trust, and keep the evidence¶
When: a script, a package or a repository you want to see run before you trust it.
Run it in a locked VM that deletes itself, then read what it tried to reach:
limabean run --ttl 1h --name probe --allow github --locked \
--repo someone/tool -- 'make install && ./tool --help'
limabean network log probe --blocked # every host it tried and was refused
--ttl keeps the VM for an hour, so you can look around in it before it
goes. The run_untrusted workflow does the
same through Claude, and reads the code with you first.
Watch: a new VM gets your shared secrets. For this, keep the shared
file empty, or check limabean secrets ls first.