How it works
Terminal and status lights
Section titled “Terminal and status lights”Each agent runs in a pseudo-terminal with a virtual screen
(charmbracelet/x/vt). internal/input passes keys through unchanged,
except for the prefix key.
internal/detect uses herdr’s screen-rule engine and detection manifests.
It prefers copies in ~/.local/state/herdr/agent-detection when they are
at least as recent as the bundled versions. herdr refreshes those copies
while running. Custom manifests in ~/.config/herdr/agent-detection
always take precedence. Manifests are © herdr contributors, Apache-2.0.
A completed tab counts as seen once it has been active for a second, with nerdr focused and no editor over it. Until then its agent is not taken as visible either, so walking past a tab does not clear its light. Inside herdr, nerdr’s pane must be focused.
The core and the holder
Section titled “The core and the holder”The nerdr you start is a client. It starts, once per folder, a holder:
a process that starts and owns every agent, terminal and plugin run,
their terminals included, and that starts nerdr’s core, which has the
tabs and all the logic. Both run on with no terminal when you close the
client, the core writing its log to ~/.local/state/nerdr/core-*.log,
and nerdr started in the folder again connects to them, by a client
socket of their own (see where it is, and what a sandbox must
deny). If the core crashes, the holder starts it
again and it takes up the programs still running. If the holder ends,
its programs end with it, and the next start resumes the conversations
as after a quit. Quit stops it all.
The programs started while a client is connected run with that client’s
environment. With NERDR_HOLDER=0, nerdr runs without a holder and core,
the agents as its own children, and they end when it does.
Resuming conversations
Section titled “Resuming conversations”Claude starts with a session ID and resumes with --resume <id>.
Reported session IDs from herdr integrations take precedence. Otherwise,
opencode and Copilot use the newest session for the worktree, and Codex
uses resume --last in that folder. If resuming fails immediately, nerdr
starts a new conversation.
Integrations
Section titled “Integrations”internal/hooks listens on ~/.local/state/nerdr/nerdr.sock using
herdr’s JSON-line protocol. Agents get NERDR_SOCKET and a secret for
their tab in NERDR_TAB. nerdr’s agent commands
and built-in hooks (nerdr hook …) use them to send requests and status
reports to the running nerdr.
Some commands do not go through the socket: nerdr why, order and
walkthrough write straight to NERDR_DATA_DIR, and nerdr search and
nerdr usage read the agents’ logs themselves (search only asks nerdr
which folders to search).
For herdr’s integrations, agents also get HERDR_ENV=1,
HERDR_PANE_ID (the same secret), HERDR_SOCKET_PATH, and
HERDR_BIN_PATH pointing to nerdr. nerdr pane report-agent … takes
the reports in place of herdr’s CLI.
For herdr’s protocol (nerdr pane), only pane.report_agent,
pane.report_agent_session and pane.release_agent are accepted. Each
tab gets a new secret at every start, so agents can report only for
their own tab. Conversation IDs that resemble
flags are rejected, and control characters are removed from text.
Reported state lasts until released or the agent restarts.
Terminal panes receive neither the socket nor herdr’s variables. Running
herdr in a shell therefore connects to herdr itself.
Automatic messages
Section titled “Automatic messages”nerdr pastes queued messages using the agent’s paste mode, then sends
Enter. It never inserts messages while the agent is asking a question.
If the agent shows no response within five seconds, nerdr reports a stall
and pauses the rest of the queue (✉n⏸) until you visit the tab, so the
messages stay in order.
Saved sessions
Section titled “Saved sessions”Sessions are stored per startup folder in
~/.local/state/nerdr/sessions/<folder>-<hash>.json. Before saving, nerdr
backs up the previous version to sessions/snapshots/, at most once every
15 minutes. It keeps the newest 20 snapshots.
To recover closed tabs, quit nerdr (Quit, not Close: a core left running
would write over it) and copy a snapshot over the session
file. Unreadable session files are preserved as ….broken-<time>.json.
herdr and the review panel
Section titled “herdr and the review panel”nerdr reports its most urgent tab through herdr pane report-agent and
switches to it when herdr focuses the pane. Tab agents report to nerdr’s
socket, so their hooks cannot overwrite that combined report.
The review panel (internal/review and related packages) follows the
active tab’s worktrees. nerdr why and nerdr order append JSON lines
under a lock through internal/store.
Knowing when an agent is done
Section titled “Knowing when an agent is done”Screen detection can mistake a pause or unfamiliar screen for completion. nerdr combines hooks, visible state and idle timers to reduce false alerts.
Claude starts with nerdr hooks passed through --settings; nothing is
installed. They report turn start, tool calls, pending questions or
permissions, and Stop at turn end. Hooks run "$NERDR_BIN" hook claude.
If nerdr is unavailable in the sandbox, screen detection takes over.
A subagent’s own events, and session starts after compaction, are
ignored, since they say nothing reliable about the main turn.
SubagentStart and SubagentStop count the subagents running. If the
main turn ends while a background subagent still runs, the agent stays
marked as working although its prompt looks idle, since Claude resumes
the main agent once the subagent is done. Only the end of the last
background subagent changes the state. Other subagent starts and stops
are ignored, since Claude also runs subagents of its own after a turn to
write summaries.
Commands and monitors run in the background work the same way, read
from the background_tasks list on Claude’s Stop: while a Bash
command started with run_in_background, or a monitor, still runs, the
agent is not done, since Claude resumes it when the command ends or the
monitor reports. If nothing else is going, its light turns cyan and the
line above the agent says how many are running; nerdr treats it as at
work in every other way. The first Stop that lists none marks it done.
A command that never ends on its own, such as a dev server, keeps the
agent from being done for as long as it runs; run those in a nerdr
terminal instead. Scheduled wakeups (/loop, ScheduleWakeup) don’t
count.
opencode reports through nerdr’s plugin, nerdr.js, which nerdr installs
along with the skills: in ~/.config/opencode/plugins/ with
install = "home", or in the tab’s .opencode/plugins/ (kept out of git)
with install = "worktree". It passes on the start and end of each turn
(session.execution.*) and permission requests to
"$NERDR_BIN" hook opencode. opencode runs plugins in its server, and
only the one it starts with --standalone has the tab’s environment, so
nerdr always starts opencode that way when it supports the flag. The
plugin reports only for the opencode nerdr started, not one that agent
runs itself, and does nothing outside nerdr. Subagents’ turns are
ignored; their permission requests count. With install = "off", or
without the plugin, screen detection is used.
Codex starts with nerdr’s hooks passed with -c, so a codex the agent
runs itself has none. They report the prompt, tool calls, permission
requests, and Stop or Interrupt (Esc) at turn end, through
"$NERDR_BIN" hook codex. Codex runs a hook only once it is trusted,
and keeps that trust in ~/.codex/config.toml (or $CODEX_HOME); trust
given on the command line is ignored, and an untrusted hook makes codex
stop at start to ask. So with install = "home" nerdr has codex trust its
hooks, through codex’s own app server, which writes the entries under
[hooks.state]. Otherwise nerdr writes nothing and passes the hooks only
if they are already trusted. nerdr also passes them only to a codex
started with --no-daemon, since a codex on the shared server runs hooks
with another tab’s environment. Subagent events are ignored, apart from
their permission requests.
Copilot reports through nerdr’s plugin, which nerdr writes along with
the skills: ~/.copilot/nerdr-plugin/ with install = "home", or the
tab’s .copilot/nerdr-plugin/ (kept out of git) with
install = "worktree". nerdr gives it to the copilot it starts with
--plugin-dir, so a copilot the agent runs itself has none, and nothing
is added to ~/.copilot/hooks/. Its hooks report the prompt, tool calls,
permission prompts (Notification; PermissionRequest also comes for
tools allowed anyway) and Stop through "$NERDR_BIN" hook copilot.
A subagent has a Stop of its own, which is ignored with the rest of its
events, apart from its permission prompts. SubagentStart and
SubagentStop keep count of the subagents going, as for claude: copilot
too prompts the agent with the news once the subagent is done. Esc ends
a turn, or dismisses a permission prompt, without any hook; see below.
To keep your own edits to the plugin, remove the marked sentence from
its description in .plugin/plugin.json. With install = "off" copilot is read from the screen.
Antigravity (agy) takes hooks only from files, so nerdr puts its own
under the key "nerdr" in agy’s hooks file, along with the skills:
~/.gemini/config/hooks.json with install = "home", or the tab’s
.agents/hooks.json with install = "worktree" (kept out of git when
nerdr made it, and not touched when the repository tracks one). The
other hooks in the file are kept as they are; set "enabled": false in
nerdr’s entry to turn its hooks off. The hooks report the model’s calls, tool
results and Stop through "$NERDR_BIN" hook agy. An agy the agent runs
reads the same file, so a hook reports only for an agy whose parent is
the nerdr it names in NERDR_AGY_HOOK. An agy started through a
wrapper, such as a sandbox, is therefore read from the screen. A subagent
runs in the same agy, in a conversation of its own with its own Stop.
Conversations that start after the agent calls invoke_subagent (the
one tool call nerdr hooks before it runs) belong to subagents and are
ignored. A Stop while subagents are still running keeps the agent
working. agy has no hook for permission prompts, and Esc ends a turn
without Stop: both are read from the screen.
Grok also takes hooks only from files, so nerdr keeps its own in
~/.grok/hooks/nerdr.json with install = "home", or the tab’s
.grok/hooks/nerdr.json with install = "worktree" (kept out of git when
nerdr made it, and not touched when the repository tracks one). A file
there without nerdr’s hooks in it is left alone, so replace it with {}
to turn them off. The hooks report through "$NERDR_BIN" hook grok: which
conversation it is in (also after /new or /resume), prompts, tool
results, permission prompts and the end of a turn. A hook reports only for
a grok whose parent is the nerdr it names in NERDR_GROK_HOOK, and not
from inside a subagent, so a grok started through a wrapper is read from
the screen. Grok runs no hooks in a folder it has not been told to trust,
and asks in every new folder once any hook exists, a worktree included.
Until you answer, the tab shows as blocked and is read from the screen.
Clear screen evidence can override hooks through herdr’s visible_*
rules. A visible dialog means the agent needs input. A question absent
for two seconds is treated as answered or dismissed. After an interrupted
turn without Stop, an idle, unchanged screen for a few seconds marks
the agent idle. For an agent whose manifest has no idle rule (see
below), a screen no rule matches, unchanged for six seconds, counts as
idle in the same way.
With screen detection alone, an agent is done after appearing idle and unchanged for 2.5 seconds, or appearing idle for 20 seconds. Unknown screens leave a working agent working. The exception is agents whose manifests lack idle rules: opencode, Copilot, Cursor, Gemini and Antigravity treat an unmatched screen as idle.