Writing plugins
A plugin adds a custom script or tool to nerdr: one that rebuilds and
starts the Docker setup, seeds a database or deploys a preview. Agents
find it with nerdr plugin list and run it with nerdr plugin run. You
find it in F5, where you can also run it directly (ui).
A plugin is a folder
Section titled “A plugin is a folder”Each plugin is a folder with a plugin.toml in it, and whatever the
plugin runs. The folder’s name is the plugin’s name: lower case letters,
digits, ., _ and -.
Plugins go in ~/.config/nerdr/plugins/NAME/, beside nerdr’s config
file. They are the same in every repository and every tab, whatever
branch it is on.
plugin.toml
Section titled “plugin.toml”title = "Docker dev"description = """Rebuilds the services chosen and restarts the local docker stack.Run it when the user asks to start, restart or rebuild the dev setup."""run = ["./run.sh"] # what an agent runsui = ["./ui.sh"] # the screen you use in F5show = ["values", "output"] # what F5 shows of an agent's runopen = true # F5 opens on it when an agent runs itconfirm = false # you say yes to each agent's runwatch = false # run goes on, and what it prints goes to the agent
[[params]]name = "repos"type = "multi"description = "The repositories to build."options = ["api", "web", "worker"]required = true
[[params]]name = "clean"type = "bool"description = "Build without the cache."descriptionis required. Agents decide from it when to run the plugin, so say what it does and when it is wanted.F5shows it too.runis the command agents run, anduiis the program you use inF5. A plugin must define at least one of them. Relative paths such as./run.shorbin/run.shresolve inside the plugin’s folder; bare command names without a path are looked up onPATH. Put the plugin’s scripts inside its folder and reference them as./name(see Safety for path and interpreter restrictions).plugin.tomlmust be a regular file of at most 1 MB (descriptionat most 8 KB, and each command argument at most 4 KB).titleis optional, a name for people.showis optional: whatF5shows of an agent’s run, beyond that it runs (see When an agent runs it).openis optional, andtrueunless set tofalse: whetherF5opens on the plugin when an agent runs it. When it is off, only the plugin’s light in the list shows that it runs.confirmis optional:trueasks you before each agent’s run (see Saying yes to each run).watchis optional:truefor a run that goes on until it is stopped, and tells the agent what it finds (see A plugin that watches).
Parameters
Section titled “Parameters”Each [[params]] is one thing the plugin takes:
| Key | Meaning |
|---|---|
name |
Its name: --repos on the command line, NERDR_PARAM_REPOS in the environment |
type |
string (the default), bool, choice (one of options) or multi (a list, of options if it has them) |
description |
What it is, for the agent and for you |
options |
What it can be |
required |
An agent must give it, or ask you for it; not with a default |
default |
Its value when it is not given |
An agent gives them as nerdr plugin run docker-dev --repos api,web --clean, or all at once with --json '{"repos":["api"]}'. If a required
parameter is missing, nerdr refuses the run and tells the agent to ask
you. A value
is at most 64 KB; a list at most 256 values, each at most 4 KB.
What run gets
Section titled “What run gets”When an agent runs nerdr plugin run, nerdr runs the plugin for it,
once you have allowed it (see Safety). nerdr runs it as you,
outside any sandbox the agent is in. So a plugin can do for an agent in
a sandbox what the sandbox stops it from doing: restart the docker
setup, or read a log elsewhere.
The plugin runs in the folder the agent ran the command in. That folder must be the tab’s folder, one of its worktrees, or a folder below them; a request for anywhere else is refused. nerdr keeps the checked folder open until the plugin starts, so swapping in a symbolic link while a run waits for your yes cannot send it elsewhere.
What the plugin prints, standard output and standard error together, goes back to the agent’s command with its exit status. If that command is interrupted or stops reading, for example when it times out, nerdr stops the run.
It gets:
- each parameter as
NERDR_PARAM_NAME: a list joined with commas, a bool astrueorfalse; - all of them as one JSON object on standard input;
NERDR_PLUGIN, its name, andNERDR_PLUGIN_DIR, its folder;- the tab’s variables, as its screen does (see A screen in F5), but not the agent’s own environment.
The parameters come from the agent. nerdr passes on only those the
plugin declares, of the right type, and one of its options when it has
them. A string, or a
multi without options, is any text the agent likes. Use it with
care: quote it ("$NERDR_PARAM_NOTE"), never run it as code (eval,
sh -c "$…"), and never let it name a file or command to run unless
you check it. When you press a in F5 to review a plugin, nerdr
highlights which parameters accept free-form text.
You can also run a plugin yourself with the same command, in a tab’s shell or outside nerdr. It then runs right there, in the foreground.
When an agent runs it
Section titled “When an agent runs it”When an agent runs a plugin, F5 opens beside the agent on that
plugin, without taking the keyboard, unless the plugin’s open is
false. The plugin’s light in the list is on while it runs, with who
started it and for how long. When the run ends, the light shows the
result: green for exit status 0, red with the status for anything else.
If you are using another of the panel’s screens, nerdr leaves it alone
and tells you about the run instead, as it does for a run in another
tab.
show sets what else F5 shows:
show |
What F5 shows of the run |
|---|---|
values |
The parameters the agent gave it |
output |
What it prints, as it prints it: what the agent gets back |
ui |
Its screen, which nerdr starts for the run if it is not running (the plugin needs a ui). This starts the screen of a plugin you allowed without you pressing enter |
A run in a terminal pane (nerdr terminal nerdr plugin run …), or one
you start yourself in a shell, is not shown: you see it where it runs.
The run, on the plugin’s screen
Section titled “The run, on the plugin’s screen”While a plugin’s screen runs, nerdr tells it about the agents’ runs in
the file named by NERDR_PLUGIN_EVENTS, one JSON object per line. The
screen can then show the run its own way: tick the boxes of the values
given, show its button pressed, list what comes back. It reads the file as it grows,
for example with tail -f "$NERDR_PLUGIN_EVENTS":
{"event":"start","run":"3f9c0a1b2c3d4e5f","agent":"1:claude","values":{"repos":["api","web"],"clean":true},"time":"2026-10-24T14:02:11+02:00"}{"event":"output","run":"3f9c0a1b2c3d4e5f","text":"building api\n"}{"event":"end","run":"3f9c0a1b2c3d4e5f","exit":0}output is what the run printed since the last output, as the agent
gets it, without control characters. A screen started during a run first
gets its start and what it has printed so far. The screen only shows
the run: run does the work, and the agent gets what run prints.
Saying yes to each run
Section titled “Saying yes to each run”Once you allow a plugin, agents can run it whenever its description fits what they are doing. Agents in a sandbox are the exception: nerdr always asks you first, since the plugin runs outside the sandbox.
To be asked every time for one plugin, such as a deploy, set
confirm = true in its plugin.toml. To be asked for every plugin, set
confirm_plugin_runs = true in nerdr’s config (settings, Plugins).
nerdr then asks you before the run, and shows the tab, the agent and
the parameters it gives. y runs it. n does not, and the agent is
told not to try again unless you ask. The agent waits about a minute and
a half; if you have not answered by then, the run does not happen. If
you are answering another question, the agent is told to try again
shortly.
A plugin’s confirm can only add questions: with
confirm_plugin_runs = true, every plugin asks, whatever its confirm
says. The question is a check with you, not a sandbox. Allow only
plugins you have read.
A plugin that watches
Section titled “A plugin that watches”Some jobs go on: watch a log for errors, a queue for failed jobs, a
build for warnings. An agent’s command cannot wait for them, since
agents cut a command off after a minute or two. With watch = true:
nerdr plugin runstarts it, as any run, and returns at once.- What it prints goes to that agent as a message, typed in when the agent is idle. What it prints while the agent is busy is added to the waiting message.
- So print only what the agent should act on, one line at a time as it
appears (
grep --line-buffered,tail -F), and drop repeated lines: a watch that prints the same error every minute sends the agent that error every minute. - It keeps running until the agent runs
nerdr plugin stop NAME, you pressson it inF5, the agent’s pane closes, or nerdr quits. Stopping a watch sendsSIGTERMto its process group, andSIGKILLfive seconds later. nerdr tells the agent when a watch stops, unless the agent stopped it. - An agent watching is not put to sleep (
sleep_after).
For example, logwatch/plugin.toml:
description = """Watches the dev server's log and prints each new error, once.Start it when the user wants errors in the log looked at as they come."""run = ["./run.sh"]watch = trueshow = ["output"]and logwatch/run.sh:
#!/bin/shtail -F -n 0 "$HOME/dev/server.log" | grep --line-buffered -E 'ERROR|panic' | awk '!seen[$0]++ { print; fflush() }'Tell the agent what to do with what it finds, for example: “Start logwatch. For each error it reports, make a tab that fixes it and opens a pull request; don’t ask me about each tab.” The agent hands each error to a new tab, whose agent fixes it and opens the pull request, and goes on listening.
A screen in F5
Section titled “A screen in F5”F5 lists the plugins, each with its description and parameters. a
shows what is in the selected plugin’s folder and y allows it. d
takes the allowance back for every version of it in the folder, so later
runs and screens wait for a new yes; a screen or watch already running
goes on. For a plugin with ui, enter starts its program below the
list, in the tab’s folder, and gives it the keyboard. It keeps running
when you switch screens. Use any terminal UI: a shell script with read, gum, fzf,
bubbletea, textual.
While the screen has the keyboard, every key goes to it except
F1-F5 and nerdr’s prefix. F5 goes back to the list, esc in the
list to the agent, and x in the list stops the program. When it ends,
its last screen stays until enter starts it again.
The screen gets the tab’s variables (NERDR_WORKTREES, a JSON list of
the tab’s worktrees, among them), NERDR_PLUGIN and NERDR_PLUGIN_DIR.
To do what an agent would, it runs its own run, or "$NERDR_BIN" plugin run NAME ….
Safety
Section titled “Safety”A plugin runs as you, outside every agent’s sandbox. Its ui screen
runs when you start it, and its run command when an agent asks for it
with parameters; for an agent in a sandbox, only once you say yes. That
is what plugins are for: letting agents start actions you have approved.
But an agent outside a sandbox, or one inside with write access to
~/.config/nerdr, could create or change plugins. So no plugin starts
or runs until you allow it:
-
F5marks a plugin “not allowed” until you do.ashows what is in its folder, each file and its text, andyallows it.enteron a screen not allowed yet asks the same, then starts it.dstops allowing every version of it in the folder again; a screen or watch already running goes on until you stop it. -
What you allow is the folder’s contents, not its name: the same files in another folder are allowed too, and renaming the folder does not ask again. Keep what you allow inside its folder.
-
nerdr plugin runrefuses a plugin not allowed, and nerdr tells you which agent wanted to run it. An agent that makes a plugin cannot have it run before you have read it. -
What you allow is everything in the folder as it was shown, an empty folder included. Any change to the folder, while you read it or later, requires approval again.
plugin.tomlis read only when it is a regular file (symlinks, FIFOs and devices are refused), and any file in the folder that a command references must exist and not be a symlink pointing outside the folder. -
Keep what a plugin runs inside its folder. A script outside it is not covered by your approval, and could be changed without you seeing it, for example in the worktree where the agent runs. Relative command paths are resolved from the plugin’s folder, and paths that lead outside it are refused. An absolute path to a tool, such as
/bin/sh, is used as it is and shown when you approve, but it is not part of the folder’s hash, so use only tools you trust.When a command runs a known interpreter or runner (
sh,bash,python,node,perl,ruby,php,lua, …, versioned names such aspython3.12andnode20,npx/bunx, orenvwith its options andNAME=valuewords), the script it is given must also be inside the plugin’s folder. A data file may be elsewhere, since nerdr neither reads nor hashes it.This check catches the common cases, not all of them. Build tools (
make,npm,docker), wrappers (timeout,xargs), module lookups (npx,python -m) and script names built at run time read files from the folder the agent runs in. Instead of calling those directly inplugin.toml, put a wrapper script in the plugin’s folder and run it as./name. -
nerdr hashes the folder just before it starts the program, in the process that then starts it. A change after that, or one made while the script runs, is not seen. A link in the folder is approved as a link: what it points to is not hashed, and a script that reads it later can read contents outside what you allowed. Keep what the script reads inside the folder.
-
An agent can have at most eight plugin runs going at once, and nerdr thirty-two in all. More wait until one ends.
-
A sandboxed agent must be able to read
~/.config/nerdr/pluginsfornerdr plugin listandnerdr plugin run: it reads the plugin’s description, parameters and key there. The allowance itself is checked by nerdr through the socket. -
Allow a plugin for what it does with any parameters an agent may give: you read the code, not the values it will get. For a plugin you want to decide on each time, set
confirm = true.
Approvals are kept in ~/.local/state/nerdr/plugins-allowed.json. An
agent in a sandbox must not be able to write it, nor
~/.config/nerdr/plugins; see
Agents in a sandbox.