Skip to content

How it works

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

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.

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.

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.

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.

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.

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.