Adding a New Agent CLI Integration

June 16, 2026 · View on GitHub

This guide explains how to add support for a new coding agent CLI to Kandev.

Agents are defined entirely in Go. The backend discovers them through a registry, and the frontend picks them up automatically via the API - no frontend changes are needed.

For a clean reference implementation, see gemini.go (ACP protocol). For a CLI-only TUI agent, see the TUIAgent declarative type in tui_agent.go.


If an agent has a CLI but no structured protocol (no JSON-RPC, no streaming API), use the TUIAgent declarative type. The agent runs in a terminal session and input/output passes through a WebSocket - users interact with the full agent TUI directly.

This is the simplest path. A complete agent definition is ~20 lines.

Step 1: Create the agent file

Create apps/backend/internal/agent/agents/{agent_id}.go:

package agents

func NewClaude() *TUIAgent {
    return NewTUIAgent(TUIAgentConfig{
        AgentID:   "claude",
        AgentName: "Claude",
        Command:   "claude",
        Desc:      "Claude Code - an agentic coding tool by Anthropic.",

        // Optional: let users select a model in profile settings.
        // The {model} placeholder is replaced with the user's chosen model at launch.
        // Omit this if the CLI doesn't accept a model flag.
        ModelFlag: NewParam("--model", "{model}"),
    })
}

Logos are optional - the UI shows a blank placeholder when none is provided. To add them, embed SVGs and pass LogoLight / LogoDark:

package agents

import _ "embed"

//go:embed logos/claude_light.svg
var claudeLogoLight []byte

//go:embed logos/claude_dark.svg
var claudeLogoDark []byte

func NewClaude() *TUIAgent {
    return NewTUIAgent(TUIAgentConfig{
        AgentID:   "claude",
        AgentName: "Claude",
        Command:   "claude",
        Desc:      "Claude Code - an agentic coding tool by Anthropic.",
        ModelFlag: NewParam("--model", "{model}"),
        LogoLight: claudeLogoLight,
        LogoDark:  claudeLogoDark,
    })
}

TUIAgentConfig fields (all optional fields have sensible defaults):

FieldRequiredDescription
AgentIDYesUnique lowercase identifier (e.g. "claude")
AgentNameYesDisplay name (e.g. "Claude")
CommandYesBinary name to run (e.g. "claude")
DescYesDescription shown in the UI
ModelFlagNoModel CLI flag, e.g. NewParam("--model", "{model}"). Lets users pick a model in profile settings. Omit if the CLI doesn't support model selection.
CommandArgsNoExtra args appended after Command (e.g. []string{"--debug"})
WaitForTermNoWhen true, delays process start until the terminal WebSocket connects and sends its first resize event. Use this for TUI apps that need accurate terminal dimensions on startup (e.g. ncurses-based UIs). Most agents don't need this. Defaults to false.
LogoLight / LogoDarkNoEmbedded SVG bytes for light/dark themes. The UI shows a blank placeholder if omitted.
DetectOptsNoCustom detection options. Defaults to WithCommand(Command) which checks if the binary is in $PATH.

Step 2: Register the agent

Add your agent to LoadDefaults() in apps/backend/internal/agent/registry/registry.go:

func (r *Registry) LoadDefaults() {
    all := []agents.Agent{
        // ... existing agents ...
        agents.NewMyAgent(), // add here
    }
    // ...
}

Step 3: Build and test

make -C apps/backend build && make -C apps/backend test && make -C apps/backend lint

Note: passthrough-only agents provide terminal interaction only. There are no structured chat messages, tool call visibility, or progress tracking in the chat UI.


Adding a New Agent (Full Integration)

For agents with a structured protocol (JSON-RPC, streaming JSON, etc.), implement the full Agent interface.

Step 1: Create the agent definition file

Create apps/backend/internal/agent/agents/{agent_id}.go.

This file implements the Agent interface. See the interface definition in agent.go for the full contract.

Embed StandardPassthrough to get CLI passthrough support for free:

type MyAgent struct {
    StandardPassthrough
}

func NewMyAgent() *MyAgent {
    return &MyAgent{
        StandardPassthrough: StandardPassthrough{
            PermSettings: myAgentPermSettings,
            Cfg: PassthroughConfig{
                Supported:      true,
                Label:          "CLI Passthrough",
                Description:    "Show terminal directly instead of chat interface",
                PassthroughCmd: NewCommand("myagent"),
                ModelFlag:      NewParam("--model", "{model}"),
                PromptFlag:     NewParam("--prompt", "{prompt}"),
                IdleTimeout:    3 * time.Second,
                BufferMaxBytes: DefaultBufferMaxBytes,
            },
        },
    }
}

Key implementation notes:

  • BuildCommand() returns the CLI command for structured protocol mode (the agent communicates via stdin/stdout with a protocol adapter). Include the protocol flag here (e.g. --acp, --stream-json).
  • Runtime() returns configuration used when running the agent in Docker or as a standalone process. Set Protocol to tell the adapter factory which transport to use.
  • IsInstalled() uses the Detect() helper with WithFileExists() to check for known installation paths. Set SupportsMCP and MCPConfigPaths on the result.
  • MCP servers in passthrough — set MCPStrategy on PassthroughConfig so the agent receives kandev's own tools server (plus any profile-configured MCP servers) when run in passthrough mode. Omitting it means no MCP servers are injected in passthrough. Pick the strategy that matches how your CLI loads MCP servers: mcpconfig.ClaudeStrategy{} (temp JSON file + --mcp-config flag), mcpconfig.CodexStrategy{} (repeated -c mcp_servers.… overrides), mcpconfig.CursorStrategy{} (project-local .cursor/mcp.json), mcpconfig.PiStrategy{} (project-local .pi/mcp.json), or mcpconfig.OpenCodeStrategy{} (temp JSON + OPENCODE_CONFIG env var). To add a strategy for a CLI with a different mechanism, implement mcpconfig.PassthroughMCPStrategy. See ADR 0014, ADR 0020, and internal/agent/mcpconfig/passthrough.go.
  • MCP project files in protocol mode — set RuntimeConfig.ProjectMCPStrategy only when the structured protocol adapter does not pass ACP session/new MCP servers through to the underlying CLI. Pi uses this to write .pi/mcp.json before the subprocess starts.
  • ListModels() can return a static list or dynamically query the CLI. Use execAndParse() from helpers.go for dynamic model discovery.
  • Use Cmd() builder for constructing commands fluently: Cmd("myagent", "--flag").Model(...).Settings(...).Build().

Step 2: Add logos (optional)

Logos are optional - the UI shows a blank placeholder when none is provided. To add them, place SVGs in:

apps/backend/internal/agent/agents/logos/{agent_id}_light.svg
apps/backend/internal/agent/agents/logos/{agent_id}_dark.svg

Embed them in your agent file:

//go:embed logos/myagent_light.svg
var myagentLogoLight []byte

//go:embed logos/myagent_dark.svg
var myagentLogoDark []byte

Step 3: Register the agent

Add your agent to LoadDefaults() in apps/backend/internal/agent/registry/registry.go:

func (r *Registry) LoadDefaults() {
    all := []agents.Agent{
        // ... existing agents ...
        agents.NewMyAgent(), // add here
    }
    // ...
}

That's it for registration. The frontend discovers agents through the API automatically.

Step 4: Choose a protocol

Check apps/backend/pkg/agent/protocol.go for available protocols:

ProtocolConstantTransportUsed By
ACPProtocolACPJSON-RPC 2.0 over stdin/stdoutGemini, Auggie
stream-jsonProtocolClaudeCodeStreaming JSON over stdin/stdoutClaude Code
CodexProtocolCodexJSON-RPC variant over stdin/stdoutCodex
OpenCodeProtocolOpenCodeREST/SSE over HTTPOpenCode
CopilotProtocolCopilotGo SDK (JSON-RPC internally)GitHub Copilot
AmpProtocolAmpStreaming JSON over stdin/stdoutAmp

If an existing protocol fits your agent, reuse it - set Protocol in Runtime(). The adapter factory in factory.go selects the right transport automatically based on this value.

If you need a new protocol, you'll need to:

  1. Add a new Protocol constant in apps/backend/pkg/agent/protocol.go
  2. Create a transport adapter in apps/backend/internal/agentctl/server/adapter/transport/{protocol}/
  3. Add the case to the factory switch in apps/backend/internal/agentctl/server/adapter/factory.go

Step 5: Build and test

make -C apps/backend build && make -C apps/backend test && make -C apps/backend lint

Start the dev server and verify:

  • The agent appears in the agent selector in the UI
  • Agent execution works (start a task, send a prompt)
  • Passthrough mode works (toggle to CLI passthrough in the session)
  • Model listing works

Updating an Existing Agent

ChangeWhere
Update CLI versionBuildCommand() and Runtime().Cmd in the agent file
Add/remove modelsListModels() - update static list or dynamic command
Update permissionsPermissionSettings() variable and PassthroughConfig
Update Docker imageRuntime() - change Image/Tag fields
Change protocolRuntime().Protocol in the agent file

File Reference

FileWhen to modify
apps/backend/internal/agent/agents/{agent_id}.goAlways - agent definition
apps/backend/internal/agent/agents/logos/{agent_id}_{light,dark}.svgOptional - agent logos (UI shows blank placeholder if omitted)
apps/backend/internal/agent/registry/registry.goAlways - register in LoadDefaults()
apps/backend/internal/agent/agents/tui_agent.goNever - provides TUIAgent declarative type (read-only reference)
apps/backend/pkg/agent/protocol.goOnly if adding a new protocol
apps/backend/internal/agentctl/server/adapter/factory.goOnly if adding a new protocol
apps/backend/internal/agentctl/server/adapter/transport/{proto}/Only if adding a new protocol
apps/backend/internal/agent/agents/passthrough.goNever - provides StandardPassthrough (read-only reference)
apps/backend/internal/agent/agents/agent.goNever - defines the Agent interface (read-only reference)