# How Nodeterm Supports Multiple Built-In and Custom Agents

> Discover how Nodeterm supports multiple built-in and custom agents using a registry-driven model, declarative configs, agent-aware permissions, and a unified hook protocol for seamless AI assistant integration.

- Repository: [eneskirca/nodeterm](https://github.com/eneskirca/nodeterm)
- Tags: how-to-guide
- Published: 2026-08-23

---

**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](https://github.com/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`](https://github.com/eneskirca/nodeterm/blob/main/src/shared/agents/config.ts)

The master definition of every agent lives in **[`src/shared/agents/config.ts`](https://github.com/eneskirca/nodeterm/blob/main/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`](https://github.com/eneskirca/nodeterm/blob/main/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`](https://github.com/eneskirca/nodeterm/blob/main/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`](https://github.com/eneskirca/nodeterm/blob/main/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`](https://github.com/eneskirca/nodeterm/blob/main/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`](https://github.com/eneskirca/nodeterm/blob/main/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:

```json
{
  "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`](https://github.com/eneskirca/nodeterm/blob/main/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`](https://github.com/eneskirca/nodeterm/blob/main/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`](https://github.com/eneskirca/nodeterm/blob/main/src/shared/agents/config.ts) |
| Permission-mode flag mapping per agent | [`src/shared/agents/approval-mode.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/shared/agents/approval-mode.ts) |
| Claude-specific CLI probing (auto-mode support) | [`src/core/claude-cli.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/core/claude-cli.ts) |
| Managed Claude account handling | [`src/core/claude-accounts-core.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/core/claude-accounts-core.ts) |
| Runtime agent state (desktop) | [`src/renderer/state/agentStatus.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/renderer/state/agentStatus.ts) |
| Runtime agent state (Server Edition) | [`src/server/agent-status.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/server/agent-status.ts) |
| Agent-related UI helpers (`canBranch`, etc.) | same [`config.ts`](https://github.com/eneskirca/nodeterm/blob/main/config.ts) (exported helpers) |
| Custom-agent merge logic | [`src/shared/agents/config.ts`](https://github.com/eneskirca/nodeterm/blob/main/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`](https://github.com/eneskirca/nodeterm/blob/main/config.ts) does the rest.
- **Agent-aware permission mapping**: [`approval-mode.ts`](https://github.com/eneskirca/nodeterm/blob/main/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`](https://github.com/eneskirca/nodeterm/blob/main/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`](https://github.com/eneskirca/nodeterm/blob/main/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`](https://github.com/eneskirca/nodeterm/blob/main/src/shared/agents/config.ts) and the IPC bridge in [`src/server/agent-status.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/server/agent-status.ts), so custom agents behave identically in the browser-based UI as they do on the desktop.