How Nodeterm Supports Multiple Built-In and Custom Agents
Nodeterm supports multiple built-in and custom agents through a registry-driven capability model that treats every AI assistant as an agent, with declarative config files, agent-aware permission mapping, and a unified hook protocol for runtime state.
Nodeterm is an open-source terminal UI that lets you run AI coding agents like Claude, Codex, Gemini, Grok, and Copilot side-by-side in a node-based canvas. The architecture in the eneskirca/nodeterm repository is built around a central agent registry, so adding a new agent — whether built-in or user-defined — requires no changes to the core UI logic. Here is exactly how it works.
The Central Agent Registry in src/shared/agents/config.ts
The master definition of every agent lives in src/shared/agents/config.ts as the exported constant AGENT_CONFIG. It is a map keyed by an AgentId (such as "claude", "codex", "gemini", "grok", or "opencode"), and each entry contains:
- Label & color — the UI text and node color used in the terminal canvas.
- Spawn command — the CLI binary that launches the agent (
claude,codex, etc.). - Capability lists — membership arrays like
RESUMABLE_AGENTS,CONTEXT_LINK_CAPABLE,MODEL_SWITCH_CAPABLE, andBRANCH_CAPABLE.
The rest of the codebase never does a direct agentId === "claude" check. Instead, every feature queries a helper function derived from the AGENT_CONFIG lists. This indirection is what makes the system future-proof: when a new agent is added, the UI automatically exposes the correct features for it.
Permission-Mode Mapping per Agent
Different CLIs expose different command-line flags for permission handling. The mapping logic in src/shared/agents/approval-mode.ts converts the UI-selected mode (manual, auto, acceptEdits, etc.) into the concrete flags each binary expects (like --permission-mode or --ask-for-approval).
The mapping is agent-aware:
- Claude supports
auto. - Gemini uses
auto_edit. - Codex has no
automode at all.
This ensures that user-selected permission modes never produce an invalid CLI invocation, regardless of which agent runs in a node.
Agent-Specific CLI Probes
Before launching an agent, the core layer may probe the installed binary to detect feature support at runtime. The probe logic for Claude is implemented in src/core/claude-cli.ts — for example, it checks whether the installed Claude version supports the auto permission mode. Other agents with version-divergent features have similar probes where needed, and the output drives the capability gate before launch.
Managed Accounts for Claude and Others
Claude can run under multiple managed accounts. The account handling code lives in src/core/claude-accounts-core.ts. It creates a per-account configuration directory (under $USER_DATA/claude-accounts/<id>) and injects CLAUDE_CONFIG_DIR into the spawned process environment.
The same pattern is reused for other agents that support multiple accounts (for example, Codex) by calling the generic custom-account APIs from the core layer.
Runtime Agent State Management
The UI keeps track of each agent's runtime state — working, waiting, blocked, done, unread flag, session id — inside a Zustand store defined in src/renderer/state/agentStatus.ts.
That store is populated from hook events (e.g., agent:status) emitted by each agent. Because the store's shape is generic and doesn't care about the agent ID, it works identically for built-in agents and user-defined customs.
Server Edition Agent Bridge
When Nodeterm runs in Server Edition, the same agent-status bridge is provided by src/server/agent-status.ts. It mirrors the desktop implementation, exposing the same IPC channels (agent:status, agent:restart, agent:subagent-activity, etc.) so that agents behave identically when accessed from a browser.
Adding a Custom Agent
Users define custom agents in their settings under settings.customAgents. Each bare-bones entry looks like this:
{
"id": "my-assistant",
"label": "My Assistant",
"baseAgent": "claude",
"spawnCommand": "my-assistant",
"extraEnv": { "MY_VAR": "value" }
}
When the settings are read, the code in src/shared/agents/config.ts merges these custom definitions with the built-in AGENT_CONFIG. The baseAgent field tells the system which capability list to inherit (e.g., "claude" provides CONTEXT_LINK_CAPABLE, MODEL_SWITCH_CAPABLE, etc.).
The merge logic also respects user-provided overrides for promptInjectionMode, permissionMode, and other per-agent options. This means a custom agent can start as a copy of Claude but modify behavior and env without touching the built-in config.
Capability Helpers in Action
Every UI feature depends on the capability lists through helper functions such as canBranch(node), canContextLink(node), canModelSwitch(node), hasHooks(node), and canPermissionMode(node). Here is how the capabilities map to UI features:
| Feature | Capability | UI Location |
|---|---|---|
| Context-link (agent ↔ agent) | CONTEXT_LINK_CAPABLE |
Node right-click → Link |
| Model switching | MODEL_SWITCH_CAPABLE |
Settings → Model dropdown |
| Session-name read/write | TITLE_READ_CAPABLE / RENAME_CAPABLE |
Header title box & auto-rename logic |
| Branch conversations | BRANCH_CAPABLE |
Terminal → Branch button |
| Canvas control verbs | CANVAS_CONTROL_CAPABLE |
Agent-provided "manage-nodeterm-canvas" skill |
| Permission-mode UI | PERMISSION_MODE_CAPABLE |
Settings → Permission Mode |
A custom agent that inherits "baseAgent": "gemini" will automatically expose Gemini's subset of capabilities in the UI — no extra code required.
Summary of the Flow
- Startup —
src/shared/agents/config.tsbuilds the completeAGENT_CONFIGby merging built-in definitions withsettings.customAgents. - Node creation — When a terminal node is created,
createAgentNodein the renderer looks up the agent'sspawnCommandand constructs the launch command line, appending permission-mode flags viawithPermissionMode. - Launch — The core
PtyManagerspawns the process (locally or over SSH) with the correct environment variables (CLAUDE_CONFIG_DIR, customextraEnv, etc.). - Hook server — Agents emit hook events (
agent:status,agent:subagent-activity, etc.); the bridge and renderer store updateagentStatus. - UI updates — components read the
agentStatusstore and call helpers likecanBranch(node)orhasHooks(node)to render the appropriate controls.
Key Files
| Purpose | File |
|---|---|
| Central agent definitions & capability lists | src/shared/agents/config.ts |
| Permission-mode flag mapping per agent | src/shared/agents/approval-mode.ts |
| Claude-specific CLI probing (auto-mode support) | src/core/claude-cli.ts |
| Managed Claude account handling | src/core/claude-accounts-core.ts |
| Runtime agent state (desktop) | src/renderer/state/agentStatus.ts |
| Runtime agent state (Server Edition) | src/server/agent-status.ts |
Agent-related UI helpers (canBranch, etc.) |
same config.ts (exported helpers) |
| Custom-agent merge logic | src/shared/agents/config.ts (reads settings.customAgents) |
Summary
- Registry-driven design: Every agent — built-in or custom — is defined in one map (
AGENT_CONFIG) with declarative capability lists, which drive every UI decision. - Plug-and-play custom agents: Users can define a new agent in settings with a
baseAgentinheritance model, and the merge logic inconfig.tsdoes the rest. - Agent-aware permission mapping:
approval-mode.tstranslates UI-picked modes into the correct flags for each agent, preventing invalid CLI calls. - Shared runtime state: The Zustand-based
agentStatusstore (desktop) and the mirrored server bridge keep the UI in sync for any agent that follows the hook protocol.
Frequently Asked Questions
How do I add a custom agent to Nodeterm?
Add an entry to settings.customAgents with fields for id, label, baseAgent, and spawnCommand (plus optional extraEnv and permissionMode). On save, src/shared/agents/config.ts merges your entry into the runtime AGENT_CONFIG, and the UI picks up its capabilities automatically.
What is the baseAgent field used for?
The baseAgent field tells Nodeterm which capability list the custom agent inherits. For example, setting "baseAgent": "gemini" gives your custom agent the same CONTEXT_LINK_CAPABLE and MODEL_SWITCH_CAPABLE flags as Gemini, so the correct UI controls turn on for that node.
Can my custom agent have its own set of permissions?
Yes. You can override permissionMode and promptInjectionMode directly in the custom agent settings, and those overrides take precedence over the inherited values. The permission-mode mapping in approval-mode.ts will then translate your choice into the correct CLI flags.
Do custom agents work in the Server Edition?
They do. The server version uses the same code path from src/shared/agents/config.ts and the IPC bridge in src/server/agent-status.ts, so custom agents behave identically in the browser-based UI as they do on the desktop.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →