Agents and sandboxes
nerdr runs the agents you already use, with their own sign-ins and
settings. An [agents.<name>] table in the config changes how one is
started: extra arguments, environment variables, a model, or a wrapper
such as a sandbox.
Commands, arguments and wrappers
Section titled “Commands, arguments and wrappers”By default, nerdr runs each agent’s command: claude, opencode,
copilot, codex, cursor-agent, gemini, agy or grok. Configure an agent
with [agents.<name>]:
| Setting | Effect |
|---|---|
command |
Replaces the command. Can include a wrapper and fixed arguments. The first word must be on your PATH. |
args |
Added after command on every launch. |
env |
Overrides inherited environment variables. NERDR_* and HERDR_* are reserved. Agents and terminals do not inherit variables naming the agent or terminal nerdr was started from, such as CLAUDECODE, TMUX or KITTY_WINDOW_ID, and get TERM_PROGRAM=nerdr. |
config_model |
opencode only: its model as provider/model. opencode 2 dropped --model, so nerdr merges it into the OPENCODE_CONFIG_CONTENT variable, which only opencode’s own server (--standalone, which nerdr always uses) reads. |
Without command, the default command is used. For example,
[agents.claude] with only args still runs claude. To choose a model,
put the agent’s own flag in args, such as --model opus for Claude.
nerdr appends session arguments last. Wrappers must pass them through.
| Agent | Session arguments |
|---|---|
| Claude | --session-id <id>, --resume <id> or --continue |
| opencode | --session <id> or --continue |
| Copilot | --session-id <id> when supported, or --resume=<id> |
| Codex | resume <id> or resume --last; also --no-daemon when supported |
| Cursor | --resume <id> or --continue |
| Gemini | --session-id <id>, --resume <id> or --resume latest |
| Antigravity | --conversation <id> or --continue |
| Grok | --session-id <id> or --resume <id> |
For example, with command = ["my-sandbox", "--", "copilot"] and
args = ["--allow-all"] under [agents.copilot], nerdr starts a new
tab with:
my-sandbox -- copilot --allow-allResuming adds --resume=<id> at the end.
Codex can share a command server across sessions. That server inherits the
first session’s environment and runs outside the wrappers of later
sessions. --no-daemon keeps the server inside each tab’s Codex process,
so it stays inside the wrapper and its reports come from the right tab.
These settings apply to new, restored, restarted and switched agents,
and to reviewers. Reviewers also get [review.args]. For opencode, nerdr
uses <command> session list to find conversations and
<command> session export <id> to prepare a handoff.
Questions from the review panel go to the tab’s agent as messages. Terminal panes and your editor run without agent wrappers.
Agents in a sandbox: sandboxed
Section titled “Agents in a sandbox: sandboxed”nerdr runs as you, outside every sandbox. An agent that can reach nerdr’s
hook socket, which every agent reports to, could otherwise ask nerdr to do what its sandbox stops it from
doing: open a terminal pane (your shell), start an agent or a tab that is
not in a sandbox, or send such an agent a task. Set sandboxed = true for
an agent whose command puts it in a sandbox:
[agents.copilot]command = ["my-sandbox", "--", "copilot"]sandboxed = trueSuch an agent:
- gets
NERDR_SANDBOXED=1, and reports to the hook socket the others do; - cannot open, restart or close terminal panes (
nerdr terminal,nerdr close), nor close an agent outside a sandbox; - can start agents, tabs (
nerdr agent,nerdr tab new) and spun-off parts only with agents that are themselvessandboxed; - can send messages to, and read the panes of, only agents started in a sandbox, and no terminals. A message it leaves for an agent that later starts outside a sandbox waits in F3 for you to approve it. Results from work it spun off are reported only to that agent;
- sees only its own tab in
nerdr tabs; - can run the plugins you allowed, but nerdr asks you before each run. Plugins run outside the sandbox, as you, since that is what they are for (see Plugins).
Reviews of a sandboxed agent’s work also run only in sandboxed agents,
automatic reviews and nerdr request-review included, and the review
picker offers only those. If the configured reviewer is not sandboxed,
nerdr picks a sandboxed agent instead, of the author’s kind if need be.
If no agent can run sandboxed, no review starts. Handoffs and questions
still work: they go to you, or to an agent you pick.
The setting takes effect when the agent next starts.
A sandboxed agent cannot pretend to be one outside a sandbox. Every agent
gets its tab’s secret in NERDR_TAB, and a process in a sandbox may be
able to read it. So on Linux and macOS, nerdr also asks the kernel which
process connects to the hook socket, and takes a request as the agent’s only
if it comes from the agent itself or a process started under it, and
that started when the kernel says the agent did, so a process given the
id of an agent that has ended is not taken for it. Any
other connection is treated as sandboxed: it is restricted as above, and
refused if the tab it names is not sandboxed. The sandbox can therefore
reach nerdr’s hook socket without that becoming a way out. This holds for
the hook socket alone: with the core on its own, the client socket must
stay out of reach even with sandboxed = true (see
below). On other systems,
nerdr cannot check the process and relies on NERDR_TAB alone, so a
sandboxed agent that reads another agent’s secret could pretend to be
it.
The same check means that an agent outside a sandbox whose commands run
somewhere else, such as in a container or on a remote machine, can use
nerdr’s commands only if you mark it sandboxed.
sandboxed does not create a sandbox; your command must do that. The
setting only stops nerdr from being a way out. From inside the sandbox,
check that the agent cannot reach other ways out, such as the D-Bus
session bus, a VS Code or browser socket in /run/user/$UID, services
on localhost, or your SSH agent: the agent gets SSH_AUTH_SOCK from your
environment, and only the sandbox decides whether it can use it.
nerdr runs Git in the agent’s worktree outside the sandbox, so the
sandbox must keep the agent from the files of the main checkout’s .git
that tell Git to run commands: .git/config, .git/hooks,
.git/config.worktree and .git/worktrees/*/config.worktree. To commit,
the agent needs the rest of .git/worktrees/<name>/, the objects and the
refs. Git reads config.worktree only when the repository has
extensions.worktreeConfig on, as git sparse-checkout turns it on.
While it is, nerdr does not run Git in a repository where an agent has
run in a sandbox, and tells you why, so sparse-checkout and sandboxed
agents do not go together in nerdr: see
Security.
nerdr also refuses host Git in repositories with sandbox history and
executable filter, textconv, external diff or merge drivers. Protected
configuration can still run a script from the agent’s worktree. Globally
installed Git LFS does not block ordinary repositories, but operations
that need LFS conversion and automatic LFS uploads are refused. Use Git
inside the sandbox, or inspect the work and use your own terminal.
Protect any files included by Git configuration too. This includes files
selected by include.path and includeIf.gitdir: they must not live in an
agent-writable worktree or be replaceable by the agent. The on-branch include
refusal does not make other included files safe to leave writable.
Host Git fetch and push are also refused in repositories with sandbox history, including SSH, HTTPS and local remotes. Authentication and signing commands cannot be used to execute worktree scripts. Required signing fails instead of producing an unsigned commit. Run these operations inside the sandbox or in your own terminal after inspecting the work. Creating a new worktree can fail when it requires a fetch; see Security.
Sandbox history survives closing tabs, deleting worktrees and restarting nerdr. Imported sandboxed work keeps that history even if the destination agent never starts. If agent settings change while a launch is being prepared, nerdr refuses that pending launch; restart the agent to use the new settings.
Text that an agent outside a sandbox reads from one inside, such as code to review, a handoff or a review, was still written by the sandboxed agent and can contain instructions. It is safest to run every agent in a sandbox, or nerdr itself in one.
Other agents
Section titled “Other agents”Add a named section with command to offer another agent in the picker:
[agents.my-agent]command = ["my-agent"]resume_args = ["--continue"] # used on resume; omitted means a new conversationdetect = "amp" # screen rules to use; defaults to the agent's namedetect selects herdr’s screen rules. nerdr has them for amp, cline,
devin, droid, hermes, kilo, kimi, kiro, letta, maki, muse, pi,
qodercli and qwen besides its own agents, and herdr does not need to be
installed. Without matching rules, the status is unknown, but the tab
still works. detect also works for built-in agents, such as one behind
a wrapper that changes its screen.
nerdr does not know where custom agents store conversations. An agent
that supports herdr reports the command that resumes its conversation,
as in herdr’s guide for agent authors:
pane report-agent … -- my-agent --resume ID. nerdr then resumes it with
that command’s arguments, after the agent’s own command and args, so a
wrapper or sandbox stays in place. The reported command must be one of the
words of command, and is ignored when the agent is sandboxed. Without
such a report, nerdr resumes with resume_args. When you switch away from
such an agent, nerdr cannot read its conversation, so it relies on a
handoff the agent writes.
What a sandbox must allow
Section titled “What a sandbox must allow”Each agent receives NERDR_BIN (the nerdr executable) and
NERDR_DATA_DIR (the data location). NERDR_BIN is the path nerdr was
started from, such as /opt/homebrew/bin/nerdr, rather than the file a
link there points to, so an agent that keeps running while nerdr is
updated still finds it. Skills use
"${NERDR_BIN:-nerdr}" why add …. This reads NERDR_DATA_DIR without
needing access to the config file.
For a sandbox restricted to the worktree, use:
install = "worktree"data_dir = ".nerdr"Allow the sandbox to run NERDR_BIN, or put nerdr on its PATH. If that
is unavailable, skills instruct the agent to append JSON lines directly
to the files in that folder.
Commands such as request-review, post-review, spinoff and handoff
need more. Each call, from a skill or from an agent’s hooks, starts a
new nerdr process that passes the request on to the nerdr running the
tab, and finds it only through the NERDR_* variables and its socket.
The sandbox must:
- pass the
NERDR_*variables through,NERDR_SOCKETandNERDR_TABamong them; - allow running
$NERDR_BIN, and the file it links to when it is a link, as Homebrew’s is; - allow connecting to the socket at
$NERDR_SOCKET. It is always~/.local/state/nerdr/nerdr.sock($XDG_STATE_HOME/nerdr/nerdr.sockwhen that is set), whatever folder nerdr starts in, so the rule can name that one path.
One nerdr has that socket at a time. A second instance started in
another folder notifies you and listens on its own socket in the same
directory, nerdr-<id>.sock. If a sandbox only allows access to
nerdr.sock, allow it to connect to all sockets in that directory so
both instances work. The sockets nerdr makes in that directory are all
hook sockets: the client socket is elsewhere and the holder’s has no
path (see below),
so allowing connections to them reaches neither.
Agent commands never write to ~/.local/state/nerdr: connecting to the
socket is all the sandbox needs there. Never let a sandbox write to
~/.local/state/nerdr or ~/.config/nerdr — an agent with write access
could create or approve plugins for itself (see
Plugins and Security).
For read access:
nerdr usagereads~/.local/state/nerdr/sessions/claude-limits.jsonfor the Claude plan’s limits and~/.codex/sessionsfor Codex’s (without them, it reports the limits as unknown). Allow onlyclaude-limits.json, not the rest ofsessions/.nerdr pluginreads~/.config/nerdr/plugins.nerdr searchreads conversation logs in~/.claude/projectsand~/.codex/sessions.
The HERDR_* variables are only for herdr’s integrations; leave them
out if the sandbox does not run those. When something is missing, the
command says which of these it is, and the skills tell the agent to show
the user that message rather than try another nerdr.
nerdr notices too. The integrations for Claude, Copilot and Codex report to nerdr through the same executable and socket while they work. If a sandboxed agent finishes a turn without any reports reaching nerdr, nerdr warns you once and shows the executable and socket paths the sandbox must allow. If a reviewer stops this way, its review status line reports that its sandbox blocked it from reaching nerdr.
What a sandbox must deny when the core runs on its own
Section titled “What a sandbox must deny when the core runs on its own”nerdr runs the agents in a process of its own, the core, under a holder
that starts each program (see
How it works),
and the nerdr you see connects to it. With NERDR_HOLDER=0 nerdr runs
them itself, the way it did before the core, they end with it, and this
section does not apply.
Closing the window, or choosing Close when you leave nerdr, leaves
the agents, terminals and plugin runs working with no one watching; start
nerdr in the same folder to connect again. Quit stops them. While no
nerdr is connected, nothing is approved for you: a .nerdr.toml setup
that needs your approval waits until you connect, and a plugin run that
needs it does not run when its time to be answered runs out. What you
allowed before, such as an approved setup or a plugin that is not asked
about at each run, still runs.
This adds two things a sandboxed agent must not reach. Everything above
still applies. A sandbox that was enough for nerdr before the core, one
that lets the agent read and connect to anything in your home folder or
in $XDG_RUNTIME_DIR, is not: it must now deny them too. When the check
below finds the agent can reach them, or cannot establish whether it can,
nerdr does not start it.
- The client socket’s folder. A client can do anything you can,
approvals included. Its socket and the key to it are in
$XDG_RUNTIME_DIR/nerdrwhenXDG_RUNTIME_DIRis set; without it, in~/.cache/nerdr/runon Linux and~/Library/Application Support/nerdr/runon macOS; never under~/.local/state/nerdr. The sandbox must deny that folder, not only writing to it: a sandbox that lets the agent read and connect to anything in your home folder is enough without the core, but not with it. nerdr also refuses a client run by one of its own programs, but a process that has left the agent’s process tree gets past that. - nerdr’s processes. nerdr’s core and the holder, the process that
starts every agent and terminal, talk over a socket with no path. A
process that may trace them (
ptrace) or take their descriptors (pidfd_getfd) could still take it. On Linux, Yama’skernel.yama.ptrace_scopeof 1 or more stops that for processes the holder started that lackCAP_SYS_PTRACE; at 0 only the sandbox stops it, with a PID namespace of its own or by denying those calls.
An agent, terminal or plugin started while you are connected builds on
the environment of the nerdr you connected with, as it would without the
core: PATH, SSH_AUTH_SOCK, DBUS_SESSION_BUS_ADDRESS and the rest
come from your terminal, not from the one that first started the core,
with nerdr’s own variables and the agent’s env added. One started with
no nerdr connected gets the environment of the last one that connected to
this core; before any has, as when the core has just started again, the
one the core started with. A program keeps the environment it started
with. What the agent can reach of it is up to its sandbox.
When a sandboxed agent starts, nerdr first checks from inside its
sandbox: it runs its own executable through the agent’s command, in
place of the agent, with the agent’s environment and folder. The check
tries to see into the client socket’s folder, connect to the socket and
read its key. On Linux it also tries to reach the holder and the core
through /proc, pidfd_getfd and ptrace; on macOS that part is not
tried. If the check gets through, nerdr does not start the agent: it
shows a notice saying what the check reached, and writes it to the
core’s log in ~/.local/state/nerdr/.
The check also tries to write where the agent must not be able to:
~/.config/nerdr, ~/.local/state/nerdr, the .git/config and
.git/hooks of the main checkouts of the tab’s repositories, and the
folder the other tabs’ worktrees are in. An agent that can write another
tab’s worktree can change the code that tab’s agent and setup command
run, outside its own sandbox. When any of them can be written, nerdr
refuses to start the agent and shows a notice naming them: tighten the
sandbox so it lets the agent write its own worktree, and of the main
checkout’s .git only what a commit needs.
The check runs before every sandboxed launch, including with
NERDR_HOLDER=0, using that launch’s folder and environment. Results are not cached: a wrapper’s policy can change
with its files, configuration, environment or working directory. After
correcting the sandbox, restart the agent to have it checked again.
nerdr puts the check where the agent’s program is in command: the last
word named as the agent is (claude, codex, cursor-agent, …, or the
section’s name for an agent nerdr does not know). If none is, as with a
single script that starts the sandbox and the agent, or the sandbox
cannot run $NERDR_BIN, nerdr says it could not check and refuses to start
the agent. Configure a wrapper that can run both the agent and the check
inside the same sandbox.
An agent’s built-in sandbox for tool commands alone cannot be verified
by this external check. Marking a plain codex command as sandboxed
does not put the check inside Codex’s tool sandbox: the probe runs outside
it and is refused if it can reach the client socket. Use an external
sandbox wrapper that also confines the probe.
The check catches a sandbox set up wrong.
It does not stop an agent that gets out some other way, and it says
nothing about D-Bus, sockets in /run/user/$UID, services on localhost,
your SSH agent or other credentials.