Skip to content

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.

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:

Terminal window
my-sandbox -- copilot --allow-all

Resuming 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.

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 = true

Such 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 themselves sandboxed;
  • 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.

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 conversation
detect = "amp" # screen rules to use; defaults to the agent's name

detect 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.

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_SOCKET and NERDR_TAB among 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.sock when 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 usage reads ~/.local/state/nerdr/sessions/claude-limits.json for the Claude plan’s limits and ~/.codex/sessions for Codex’s (without them, it reports the limits as unknown). Allow only claude-limits.json, not the rest of sessions/.
  • nerdr plugin reads ~/.config/nerdr/plugins.
  • nerdr search reads conversation logs in ~/.claude/projects and ~/.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/nerdr when XDG_RUNTIME_DIR is set; without it, in ~/.cache/nerdr/run on Linux and ~/Library/Application Support/nerdr/run on 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’s kernel.yama.ptrace_scope of 1 or more stops that for processes the holder started that lack CAP_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.