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, and BRANCH_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 auto mode 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

  1. Startup — src/shared/agents/config.ts builds the complete AGENT_CONFIG by merging built-in definitions with settings.customAgents.
  2. Node creation — When a terminal node is created, createAgentNode in the renderer looks up the agent's spawnCommand and constructs the launch command line, appending permission-mode flags via withPermissionMode.
  3. Launch — The core PtyManager spawns the process (locally or over SSH) with the correct environment variables (CLAUDE_CONFIG_DIR, custom extraEnv, etc.).
  4. Hook server — Agents emit hook events (agent:status, agent:subagent-activity, etc.); the bridge and renderer store update agentStatus.
  5. UI updates — components read the agentStatus store and call helpers like canBranch(node) or hasHooks(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 baseAgent inheritance model, and the merge logic in config.ts does the rest.
  • Agent-aware permission mapping: approval-mode.ts translates UI-picked modes into the correct flags for each agent, preventing invalid CLI calls.
  • Shared runtime state: The Zustand-based agentStatus store (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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →