· limen

Our machines, through pull requests

We let an AI agent look after the machines of a home network with limen. It never gets a shell, and it never gets to decide what it may do: when it needs a new script or a new permission, it asks for it in a pull request, and a person merges it or not.

What the agent can do

limen is an MCP server, the way agents such as Claude Code call tools, that reaches each machine over SSH. On each machine, a limen.toml says which files the agent can read and which packs of scripts it can run, and every script becomes a tool with the arguments its header declares. Nothing else: no shell, no command line, and limen checks every argument before the script runs.

So the question that matters is who writes the packs and the limen.toml. In our case, the agent does, and a person approves.

Everything in a repository

Each machine's configuration and packs live in a private Git repository:

infra/
  packs/systemd/  packs/docker/   shared packs
  nodes/nas/limen.toml            what the agent may read and run
  nodes/nas/sync                  the main branch, then apply
  nodes/nas/apply                 a helper, run by sync

Only the first clone is made by hand, when a machine is set up, with a token that can only read, kept under /etc/limen/, a folder limen never lets the agent read. Its limen.toml is a link into that clone.

The agent proposes, a person merges

When the agent needs something it can't do yet, it can't change the machine to get it. It changes the repository instead, and opens a pull request. Say it wants to read nginx's logs and reload it after a change:

# nodes/nas/limen.toml
  [files]
- allow = ["/opt/state/nodes/nas/**"]
+ allow = ["/opt/state/nodes/nas/**", "/var/log/nginx/*.log"]

# nodes/nas/reload_nginx, new
+ #!/bin/sh
+ #: description = "Checks nginx's configuration and reloads it"
+ set -eu
+ nginx -t
+ exec nginx -s reload

A person reviews it on GitHub like any other change: the script that will run as root, the arguments limen will check, the paths the agent wants to read. Once it is merged, we ask the agent to sync, and nobody opens a shell: the agent calls sync, one of the machine's own scripts, which brings the clone to main and applies it. From then on reload_nginx is a tool the agent can call.

# nodes/nas/sync, what the agent runs on the machine (abridged)
#: description = "Brings the machine to the repository's main branch and applies it"
cd /opt/state
git fetch --depth 1 origin main
git reset --hard origin/main
exec ./nodes/"$LIMEN_NODE"/apply

Why it holds

What we don't do

limen can also ask a person before each run of a script that changes things. We don't use it: the review happens once, on the pull request, and the scripts on offer are the ones we are fine with the agent running whenever it decides to, such as restarting a container, clearing old images or syncing. Anything we'd want to confirm every time would go behind that question instead.

The setup, step by step, is in limen's docs/scripts.md.